diff options
| author | Kumar Kartikeya Dwivedi <memxor@gmail.com> | 2026-09-04 10:43:19 +0200 |
|---|---|---|
| committer | Alexei Starovoitov <ast@kernel.org> | 2026-09-04 07:58:36 -0700 |
| commit | 6668ed271eaefaa63e686bdfbedaeb7b8e492722 (patch) | |
| tree | 58e8288fead484f6755ad0815fc74c3742663761 /drivers/virt/coco | |
| download | linux-stable-6668ed271eaefaa63e686bdfbedaeb7b8e492722.tar.gz linux-stable-6668ed271eaefaa63e686bdfbedaeb7b8e492722.zip | |
selftests/bpf: Reject graph kptr use after RCU unlockgrafted
Add a sleepable verifier test that loads a graph-node local kptr in an
explicit RCU read-side critical section, then passes its node to
bpf_rbtree_remove() after the section ends.
Before the verifier fix, the stale NON_OWN_REF flag makes the node look like
a live borrowed reference and the program is accepted. After the fix, the
pointer is demoted without NON_OWN_REF and the graph kfunc argument is
rejected.
Also exercise a graph kptr loaded while a spin lock provides implicit RCU
protection. The pointer must be invalidated when the lock is released, which
guards the required ordering between non-owning-reference invalidation and
RCU demotion.
Update the existing fault-protected load test state description. The
post-unlock pointer no longer carries NON_OWN_REF, but remains readable
because the load is rewritten to use BPF_PROBE_MEM.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
Link: https://lore.kernel.org/r/20260904084325.52250-7-memxor@gmail.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
Diffstat (limited to 'drivers/virt/coco')
25 files changed, 3117 insertions, 0 deletions
diff --git a/drivers/virt/coco/Kconfig b/drivers/virt/coco/Kconfig new file mode 100644 index 000000000..f7691f64f --- /dev/null +++ b/drivers/virt/coco/Kconfig @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Confidential computing related collateral +# + +if VIRT_DRIVERS +source "drivers/virt/coco/efi_secret/Kconfig" + +source "drivers/virt/coco/pkvm-guest/Kconfig" + +source "drivers/virt/coco/sev-guest/Kconfig" + +source "drivers/virt/coco/tdx-guest/Kconfig" + +source "drivers/virt/coco/arm-cca-guest/Kconfig" + +source "drivers/virt/coco/guest/Kconfig" +endif + +source "drivers/virt/coco/tdx-host/Kconfig" + +config TSM + bool diff --git a/drivers/virt/coco/Makefile b/drivers/virt/coco/Makefile new file mode 100644 index 000000000..b323b0ae4 --- /dev/null +++ b/drivers/virt/coco/Makefile @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Confidential computing related collateral +# +obj-$(CONFIG_EFI_SECRET) += efi_secret/ +obj-$(CONFIG_ARM_PKVM_GUEST) += pkvm-guest/ +obj-$(CONFIG_SEV_GUEST) += sev-guest/ +obj-$(CONFIG_INTEL_TDX_GUEST) += tdx-guest/ +obj-$(CONFIG_INTEL_TDX_HOST) += tdx-host/ +obj-$(CONFIG_ARM_CCA_GUEST) += arm-cca-guest/ +obj-$(CONFIG_TSM) += tsm-core.o +obj-$(CONFIG_TSM_GUEST) += guest/ diff --git a/drivers/virt/coco/arm-cca-guest/Kconfig b/drivers/virt/coco/arm-cca-guest/Kconfig new file mode 100644 index 000000000..3f0f013f0 --- /dev/null +++ b/drivers/virt/coco/arm-cca-guest/Kconfig @@ -0,0 +1,10 @@ +config ARM_CCA_GUEST + tristate "Arm CCA Guest driver" + depends on ARM64 + select TSM_REPORTS + help + The driver provides userspace interface to request and + attestation report from the Realm Management Monitor(RMM). + + If you choose 'M' here, this module will be called + arm-cca-guest. diff --git a/drivers/virt/coco/arm-cca-guest/Makefile b/drivers/virt/coco/arm-cca-guest/Makefile new file mode 100644 index 000000000..69eeba08e --- /dev/null +++ b/drivers/virt/coco/arm-cca-guest/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +obj-$(CONFIG_ARM_CCA_GUEST) += arm-cca-guest.o diff --git a/drivers/virt/coco/arm-cca-guest/arm-cca-guest.c b/drivers/virt/coco/arm-cca-guest/arm-cca-guest.c new file mode 100644 index 000000000..0eeddd1ff --- /dev/null +++ b/drivers/virt/coco/arm-cca-guest/arm-cca-guest.c @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2023 ARM Ltd. + */ + +#include <linux/arm-smccc.h> +#include <linux/cc_platform.h> +#include <linux/kernel.h> +#include <linux/device-id/platform.h> +#include <linux/module.h> +#include <linux/smp.h> +#include <linux/tsm.h> +#include <linux/types.h> + +#include <asm/rsi.h> + +/** + * struct arm_cca_token_info - a descriptor for the token buffer. + * @granule: PA of the granule to which the token will be written + * @offset: Offset within granule to start of buffer in bytes + */ +struct arm_cca_token_info { + phys_addr_t granule; + unsigned long offset; +}; + +/** + * arm_cca_attestation_continue - Retrieve the attestation token data. + * + * @info: pointer to the arm_cca_token_info + * + * Attestation token generation is a long running operation and therefore + * the token data may not be retrieved in a single call. Moreover, the + * token retrieval operation must be requested on the same CPU on which the + * attestation token generation was initialised. + * This helper function must therefore be executed on the same CPU multiple + * times until the entire token data is retrieved. + */ +static unsigned long +arm_cca_attestation_continue(struct arm_cca_token_info *info) +{ + unsigned long ret; + unsigned long len; + unsigned long size; + + size = RSI_GRANULE_SIZE - info->offset; + ret = rsi_attestation_token_continue(info->granule, info->offset, size, + &len); + info->offset += len; + return ret; +} + +/** + * arm_cca_report_new - Generate a new attestation token. + * + * @report: pointer to the TSM report context information. + * @data: pointer to the context specific data for this module. + * + * Initialise the attestation token generation using the challenge data + * passed in the TSM descriptor. Allocate memory for the attestation token + * and retrieve the attestation token on the same CPU on which the + * attestation token generation was initialised. + * + * The challenge data must be at least 32 bytes and no more than 64 bytes. If + * less than 64 bytes are provided it will be zero padded to 64 bytes. + * + * Return: + * * %0 - Attestation token generated successfully. + * * %-EINVAL - A parameter was not valid. + * * %-ENOMEM - Out of memory. + * * %-EFAULT - Failed to get IPA for memory page(s). + */ +static int arm_cca_report_new(struct tsm_report *report, void *data) +{ + int ret = 0; + unsigned long rsi_result; + long max_size; + unsigned long token_size = 0; + struct arm_cca_token_info info; + void *buf; + u8 *token __free(kvfree) = NULL; + struct tsm_report_desc *desc = &report->desc; + + if (desc->inblob_len < 32 || desc->inblob_len > 64) + return -EINVAL; + + /* + * The attestation token 'init' and 'continue' calls must be + * performed on the same CPU, so disable CPU migration around + * those operations. + */ + migrate_disable(); + + max_size = rsi_attestation_token_init(desc->inblob, desc->inblob_len); + if (max_size <= 0) { + ret = -EINVAL; + goto exit_migrate_enable; + } + + /* Allocate outblob */ + token = kvzalloc(max_size, GFP_KERNEL); + if (!token) { + ret = -ENOMEM; + goto exit_migrate_enable; + } + + /* + * Since the outblob may not be physically contiguous, use a page + * to bounce the buffer from RMM. + */ + buf = alloc_pages_exact(RSI_GRANULE_SIZE, GFP_KERNEL); + if (!buf) { + ret = -ENOMEM; + goto exit_migrate_enable; + } + + /* Get the PA of the memory page(s) that were allocated */ + info.granule = (unsigned long)virt_to_phys(buf); + + /* Loop until the token is ready or there is an error */ + do { + /* Retrieve one RSI_GRANULE_SIZE data per loop iteration */ + info.offset = 0; + do { + /* + * Retrieve a sub-granule chunk of data per loop + * iteration. + */ + rsi_result = arm_cca_attestation_continue(&info); + } while (rsi_result == RSI_INCOMPLETE && + info.offset < RSI_GRANULE_SIZE); + + /* Break out in case of failure */ + if (rsi_result != RSI_SUCCESS && rsi_result != RSI_INCOMPLETE) { + ret = -ENXIO; + token_size = 0; + goto exit_free_granule_page; + } + + /* + * Copy the retrieved token data from the granule + * to the token buffer, ensuring that the RMM doesn't + * overflow the buffer. + */ + if (WARN_ON(token_size + info.offset > max_size)) + break; + memcpy(&token[token_size], buf, info.offset); + token_size += info.offset; + } while (rsi_result == RSI_INCOMPLETE); + + report->outblob = no_free_ptr(token); +exit_free_granule_page: + report->outblob_len = token_size; + free_pages_exact(buf, RSI_GRANULE_SIZE); +exit_migrate_enable: + migrate_enable(); + return ret; +} + +static const struct tsm_report_ops arm_cca_tsm_ops = { + .name = KBUILD_MODNAME, + .report_new = arm_cca_report_new, +}; + +/** + * arm_cca_guest_init - Register with the Trusted Security Module (TSM) + * interface. + * + * Return: + * * %0 - Registered successfully with the TSM interface. + * * %-ENODEV - The execution context is not an Arm Realm. + * * %-EBUSY - Already registered. + */ +static int __init arm_cca_guest_init(void) +{ + int ret; + + if (!is_realm_world()) + return -ENODEV; + + ret = tsm_report_register(&arm_cca_tsm_ops, NULL); + if (ret < 0) + pr_err("Error %d registering with TSM\n", ret); + + return ret; +} +module_init(arm_cca_guest_init); + +/** + * arm_cca_guest_exit - unregister with the Trusted Security Module (TSM) + * interface. + */ +static void __exit arm_cca_guest_exit(void) +{ + tsm_report_unregister(&arm_cca_tsm_ops); +} +module_exit(arm_cca_guest_exit); + +/* modalias, so userspace can autoload this module when RSI is available */ +static const struct platform_device_id arm_cca_match[] __maybe_unused = { + { .name = RSI_PDEV_NAME }, + { } +}; + +MODULE_DEVICE_TABLE(platform, arm_cca_match); +MODULE_AUTHOR("Sami Mujawar <sami.mujawar@arm.com>"); +MODULE_DESCRIPTION("Arm CCA Guest TSM Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/virt/coco/efi_secret/Kconfig b/drivers/virt/coco/efi_secret/Kconfig new file mode 100644 index 000000000..94d88e5da --- /dev/null +++ b/drivers/virt/coco/efi_secret/Kconfig @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-only +config EFI_SECRET + tristate "EFI secret area securityfs support" + depends on EFI && (X86_64 || ARM64) + select EFI_COCO_SECRET + select SECURITYFS + help + This is a driver for accessing the EFI secret area via securityfs. + The EFI secret area is a memory area designated by the firmware for + confidential computing secret injection (for example for AMD SEV + guests). The driver exposes the secrets as files in + <securityfs>/secrets/coco. Files can be read and deleted (deleting + a file wipes the secret from memory). + + To compile this driver as a module, choose M here. + The module will be called efi_secret. diff --git a/drivers/virt/coco/efi_secret/Makefile b/drivers/virt/coco/efi_secret/Makefile new file mode 100644 index 000000000..c7047ce80 --- /dev/null +++ b/drivers/virt/coco/efi_secret/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +obj-$(CONFIG_EFI_SECRET) += efi_secret.o diff --git a/drivers/virt/coco/efi_secret/efi_secret.c b/drivers/virt/coco/efi_secret/efi_secret.c new file mode 100644 index 000000000..5946c5abe --- /dev/null +++ b/drivers/virt/coco/efi_secret/efi_secret.c @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * efi_secret module + * + * Copyright (C) 2022 IBM Corporation + * Author: Dov Murik <dovmurik@linux.ibm.com> + */ + +/** + * DOC: efi_secret: Allow reading EFI confidential computing (coco) secret area + * via securityfs interface. + * + * When the module is loaded (and securityfs is mounted, typically under + * /sys/kernel/security), a "secrets/coco" directory is created in securityfs. + * In it, a file is created for each secret entry. The name of each such file + * is the GUID of the secret entry, and its content is the secret data. + */ + +#include <linux/platform_device.h> +#include <linux/seq_file.h> +#include <linux/fs.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/module.h> +#include <linux/io.h> +#include <linux/security.h> +#include <linux/efi.h> +#include <linux/cacheflush.h> + +#define EFI_SECRET_NUM_FILES 64 + +struct efi_secret { + struct dentry *secrets_dir; + void __iomem *secret_data; + u64 secret_data_len; +}; + +/* + * Structure of the EFI secret area + * + * Offset Length + * (bytes) (bytes) Usage + * ------- ------- ----- + * 0 16 Secret table header GUID (must be 1e74f542-71dd-4d66-963e-ef4287ff173b) + * 16 4 Length of bytes of the entire secret area + * + * 20 16 First secret entry's GUID + * 36 4 First secret entry's length in bytes (= 16 + 4 + x) + * 40 x First secret entry's data + * + * 40+x 16 Second secret entry's GUID + * 56+x 4 Second secret entry's length in bytes (= 16 + 4 + y) + * 60+x y Second secret entry's data + * + * (... and so on for additional entries) + * + * The GUID of each secret entry designates the usage of the secret data. + */ + +/** + * struct secret_header - Header of entire secret area; this should be followed + * by instances of struct secret_entry. + * @guid: Must be EFI_SECRET_TABLE_HEADER_GUID + * @len: Length in bytes of entire secret area, including header + */ +struct secret_header { + efi_guid_t guid; + u32 len; +} __attribute((packed)); + +/** + * struct secret_entry - Holds one secret entry + * @guid: Secret-specific GUID (or NULL_GUID if this secret entry was deleted) + * @len: Length of secret entry, including its guid and len fields + * @data: The secret data (full of zeros if this secret entry was deleted) + */ +struct secret_entry { + efi_guid_t guid; + u32 len; + u8 data[]; +} __attribute((packed)); + +static size_t secret_entry_data_len(struct secret_entry *e) +{ + return e->len - sizeof(*e); +} + +static struct efi_secret the_efi_secret; + +static inline struct efi_secret *efi_secret_get(void) +{ + return &the_efi_secret; +} + +static int efi_secret_bin_file_show(struct seq_file *file, void *data) +{ + struct secret_entry *e = file->private; + + if (e) + seq_write(file, e->data, secret_entry_data_len(e)); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(efi_secret_bin_file); + +/* + * Overwrite memory content with zeroes, and ensure that dirty cache lines are + * actually written back to memory, to clear out the secret. + */ +static void wipe_memory(void *addr, size_t size) +{ + memzero_explicit(addr, size); +#ifdef CONFIG_X86 + clflush_cache_range(addr, size); +#endif +} + +static int efi_secret_unlink(struct inode *dir, struct dentry *dentry) +{ + struct inode *inode = d_inode(dentry); + struct secret_entry *e = (struct secret_entry *)inode->i_private; + + if (e) { + /* Zero out the secret data */ + wipe_memory(e->data, secret_entry_data_len(e)); + e->guid = NULL_GUID; + } + + inode->i_private = NULL; + + return simple_unlink(inode, dentry); +} + +static const struct inode_operations efi_secret_dir_inode_operations = { + .lookup = simple_lookup, + .unlink = efi_secret_unlink, +}; + +static int efi_secret_map_area(struct platform_device *dev) +{ + int ret; + struct efi_secret *s = efi_secret_get(); + struct linux_efi_coco_secret_area *secret_area; + + if (efi.coco_secret == EFI_INVALID_TABLE_ADDR) { + dev_err(&dev->dev, "Secret area address is not available\n"); + return -EINVAL; + } + + secret_area = memremap(efi.coco_secret, sizeof(*secret_area), MEMREMAP_WB); + if (secret_area == NULL) { + dev_err(&dev->dev, "Could not map secret area EFI config entry\n"); + return -ENOMEM; + } + if (!secret_area->base_pa || secret_area->size < sizeof(struct secret_header)) { + dev_err(&dev->dev, + "Invalid secret area memory location (base_pa=0x%llx size=0x%llx)\n", + secret_area->base_pa, secret_area->size); + ret = -EINVAL; + goto unmap; + } + + s->secret_data = ioremap_encrypted(secret_area->base_pa, secret_area->size); + if (s->secret_data == NULL) { + dev_err(&dev->dev, "Could not map secret area\n"); + ret = -ENOMEM; + goto unmap; + } + + s->secret_data_len = secret_area->size; + ret = 0; + +unmap: + memunmap(secret_area); + return ret; +} + +static void efi_secret_securityfs_teardown(struct platform_device *dev) +{ + struct efi_secret *s = efi_secret_get(); + + securityfs_remove(s->secrets_dir); + s->secrets_dir = NULL; + + dev_dbg(&dev->dev, "Removed securityfs entries\n"); +} + +static int efi_secret_securityfs_setup(struct platform_device *dev) +{ + struct efi_secret *s = efi_secret_get(); + int ret = 0, i = 0, bytes_left; + unsigned char *ptr; + struct secret_header *h; + struct secret_entry *e; + struct dentry *dent, *dir; + char guid_str[EFI_VARIABLE_GUID_LEN + 1]; + + ptr = (void __force *)s->secret_data; + h = (struct secret_header *)ptr; + if (efi_guidcmp(h->guid, EFI_SECRET_TABLE_HEADER_GUID)) { + /* + * This is not an error: it just means that EFI defines secret + * area but it was not populated by the Guest Owner. + */ + dev_dbg(&dev->dev, "EFI secret area does not start with correct GUID\n"); + return -ENODEV; + } + if (h->len < sizeof(*h)) { + dev_err(&dev->dev, "EFI secret area reported length is too small\n"); + return -EINVAL; + } + if (h->len > s->secret_data_len) { + dev_err(&dev->dev, "EFI secret area reported length is too big\n"); + return -EINVAL; + } + + s->secrets_dir = NULL; + + dent = securityfs_create_dir("secrets", NULL); + if (IS_ERR(dent)) { + dev_err(&dev->dev, "Error creating secrets securityfs directory entry err=%ld\n", + PTR_ERR(dent)); + return PTR_ERR(dent); + } + s->secrets_dir = dent; + + dir = securityfs_create_dir("coco", s->secrets_dir); + if (IS_ERR(dir)) { + dev_err(&dev->dev, "Error creating coco securityfs directory entry err=%ld\n", + PTR_ERR(dir)); + return PTR_ERR(dir); + } + d_inode(dir)->i_op = &efi_secret_dir_inode_operations; + + bytes_left = h->len - sizeof(*h); + ptr += sizeof(*h); + while (bytes_left >= (int)sizeof(*e) && i < EFI_SECRET_NUM_FILES) { + e = (struct secret_entry *)ptr; + if (e->len < sizeof(*e) || e->len > (unsigned int)bytes_left) { + dev_err(&dev->dev, "EFI secret area is corrupted\n"); + ret = -EINVAL; + goto err_cleanup; + } + + /* Skip deleted entries (which will have NULL_GUID) */ + if (efi_guidcmp(e->guid, NULL_GUID)) { + efi_guid_to_str(&e->guid, guid_str); + + dent = securityfs_create_file(guid_str, 0440, dir, (void *)e, + &efi_secret_bin_file_fops); + if (IS_ERR(dent)) { + dev_err(&dev->dev, "Error creating efi_secret securityfs entry\n"); + ret = PTR_ERR(dent); + goto err_cleanup; + } + i++; + } + ptr += e->len; + bytes_left -= e->len; + } + + dev_info(&dev->dev, "Created %d entries in securityfs secrets/coco\n", i); + return 0; + +err_cleanup: + efi_secret_securityfs_teardown(dev); + return ret; +} + +static void efi_secret_unmap_area(void) +{ + struct efi_secret *s = efi_secret_get(); + + if (s->secret_data) { + iounmap(s->secret_data); + s->secret_data = NULL; + s->secret_data_len = 0; + } +} + +static int efi_secret_probe(struct platform_device *dev) +{ + int ret; + + ret = efi_secret_map_area(dev); + if (ret) + return ret; + + ret = efi_secret_securityfs_setup(dev); + if (ret) + goto err_unmap; + + return ret; + +err_unmap: + efi_secret_unmap_area(); + return ret; +} + +static void efi_secret_remove(struct platform_device *dev) +{ + efi_secret_securityfs_teardown(dev); + efi_secret_unmap_area(); +} + +static struct platform_driver efi_secret_driver = { + .probe = efi_secret_probe, + .remove = efi_secret_remove, + .driver = { + .name = "efi_secret", + }, +}; + +module_platform_driver(efi_secret_driver); + +MODULE_DESCRIPTION("Confidential computing EFI secret area access"); +MODULE_AUTHOR("IBM"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:efi_secret"); diff --git a/drivers/virt/coco/guest/Kconfig b/drivers/virt/coco/guest/Kconfig new file mode 100644 index 000000000..da570dc4b --- /dev/null +++ b/drivers/virt/coco/guest/Kconfig @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Confidential computing shared guest collateral +# +config TSM_GUEST + bool + +config TSM_REPORTS + select TSM_GUEST + select CONFIGFS_FS + tristate + +config TSM_MEASUREMENTS + select TSM_GUEST + select CRYPTO_HASH_INFO + bool diff --git a/drivers/virt/coco/guest/Makefile b/drivers/virt/coco/guest/Makefile new file mode 100644 index 000000000..9ec4860bd --- /dev/null +++ b/drivers/virt/coco/guest/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_TSM_REPORTS) += tsm_report.o +tsm_report-y := report.o +obj-$(CONFIG_TSM_MEASUREMENTS) += tsm-mr.o diff --git a/drivers/virt/coco/guest/report.c b/drivers/virt/coco/guest/report.c new file mode 100644 index 000000000..b254a1416 --- /dev/null +++ b/drivers/virt/coco/guest/report.c @@ -0,0 +1,539 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright(c) 2023 Intel Corporation. All rights reserved. */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/tsm.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/rwsem.h> +#include <linux/string.h> +#include <linux/module.h> +#include <linux/cleanup.h> +#include <linux/configfs.h> + +static struct tsm_provider { + const struct tsm_report_ops *ops; + void *data; + atomic_t count; +} provider; +static DECLARE_RWSEM(tsm_rwsem); + +/** + * DOC: Trusted Security Module (TSM) Attestation Report Interface + * + * The TSM report interface is a common provider of blobs that facilitate + * attestation of a TVM (confidential computing guest) by an attestation + * service. A TSM report combines a user-defined blob (likely a public-key with + * a nonce for a key-exchange protocol) with a signed attestation report. That + * combined blob is then used to obtain secrets provided by an agent that can + * validate the attestation report. The expectation is that this interface is + * invoked infrequently, however configfs allows for multiple agents to + * own their own report generation instances to generate reports as + * often as needed. + * + * The attestation report format is TSM provider specific, when / if a standard + * materializes that can be published instead of the vendor layout. Until then + * the 'provider' attribute indicates the format of 'outblob', and optionally + * 'auxblob' and 'manifestblob'. + */ + +struct tsm_report_state { + struct tsm_report report; + unsigned long write_generation; + unsigned long read_generation; + struct config_item cfg; +}; + +enum tsm_data_select { + TSM_REPORT, + TSM_CERTS, + TSM_MANIFEST, +}; + +static struct tsm_report *to_tsm_report(struct config_item *cfg) +{ + struct tsm_report_state *state = + container_of(cfg, struct tsm_report_state, cfg); + + return &state->report; +} + +static struct tsm_report_state *to_state(struct tsm_report *report) +{ + return container_of(report, struct tsm_report_state, report); +} + +static int try_advance_write_generation(struct tsm_report *report) +{ + struct tsm_report_state *state = to_state(report); + + lockdep_assert_held_write(&tsm_rwsem); + + /* + * Malicious or broken userspace has written enough times for + * read_generation == write_generation by modular arithmetic without an + * interim read. Stop accepting updates until the current report + * configuration is read. + */ + if (state->write_generation == state->read_generation - 1) + return -EBUSY; + state->write_generation++; + return 0; +} + +static ssize_t tsm_report_privlevel_store(struct config_item *cfg, + const char *buf, size_t len) +{ + struct tsm_report *report = to_tsm_report(cfg); + unsigned int val; + int rc; + + rc = kstrtouint(buf, 0, &val); + if (rc) + return rc; + + guard(rwsem_write)(&tsm_rwsem); + if (!provider.ops) + return -ENXIO; + + /* + * The valid privilege levels that a TSM might accept, if it accepts a + * privilege level setting at all, are a max of TSM_PRIVLEVEL_MAX (see + * SEV-SNP GHCB) and a minimum of a TSM selected floor value no less + * than 0. + */ + if (provider.ops->privlevel_floor > val || val > TSM_REPORT_PRIVLEVEL_MAX) + return -EINVAL; + + rc = try_advance_write_generation(report); + if (rc) + return rc; + report->desc.privlevel = val; + + return len; +} +CONFIGFS_ATTR_WO(tsm_report_, privlevel); + +static ssize_t tsm_report_privlevel_floor_show(struct config_item *cfg, + char *buf) +{ + guard(rwsem_read)(&tsm_rwsem); + + if (!provider.ops) + return -ENXIO; + + return sysfs_emit(buf, "%u\n", provider.ops->privlevel_floor); +} +CONFIGFS_ATTR_RO(tsm_report_, privlevel_floor); + +static ssize_t tsm_report_service_provider_store(struct config_item *cfg, + const char *buf, size_t len) +{ + struct tsm_report *report = to_tsm_report(cfg); + size_t sp_len; + char *sp; + int rc; + + guard(rwsem_write)(&tsm_rwsem); + rc = try_advance_write_generation(report); + if (rc) + return rc; + + sp_len = (buf[len - 1] != '\n') ? len : len - 1; + + sp = kstrndup(buf, sp_len, GFP_KERNEL); + if (!sp) + return -ENOMEM; + kfree(report->desc.service_provider); + + report->desc.service_provider = sp; + + return len; +} +CONFIGFS_ATTR_WO(tsm_report_, service_provider); + +static ssize_t tsm_report_service_guid_store(struct config_item *cfg, + const char *buf, size_t len) +{ + struct tsm_report *report = to_tsm_report(cfg); + int rc; + + guard(rwsem_write)(&tsm_rwsem); + rc = try_advance_write_generation(report); + if (rc) + return rc; + + report->desc.service_guid = guid_null; + + rc = guid_parse(buf, &report->desc.service_guid); + if (rc) + return rc; + + return len; +} +CONFIGFS_ATTR_WO(tsm_report_, service_guid); + +static ssize_t tsm_report_service_manifest_version_store(struct config_item *cfg, + const char *buf, size_t len) +{ + struct tsm_report *report = to_tsm_report(cfg); + unsigned int val; + int rc; + + rc = kstrtouint(buf, 0, &val); + if (rc) + return rc; + + guard(rwsem_write)(&tsm_rwsem); + rc = try_advance_write_generation(report); + if (rc) + return rc; + report->desc.service_manifest_version = val; + + return len; +} +CONFIGFS_ATTR_WO(tsm_report_, service_manifest_version); + +static ssize_t tsm_report_inblob_write(struct config_item *cfg, + const void *buf, size_t count) +{ + struct tsm_report *report = to_tsm_report(cfg); + int rc; + + guard(rwsem_write)(&tsm_rwsem); + rc = try_advance_write_generation(report); + if (rc) + return rc; + + report->desc.inblob_len = count; + memcpy(report->desc.inblob, buf, count); + return count; +} +CONFIGFS_BIN_ATTR_WO(tsm_report_, inblob, NULL, TSM_REPORT_INBLOB_MAX); + +static ssize_t tsm_report_generation_show(struct config_item *cfg, char *buf) +{ + struct tsm_report *report = to_tsm_report(cfg); + struct tsm_report_state *state = to_state(report); + + guard(rwsem_read)(&tsm_rwsem); + return sysfs_emit(buf, "%lu\n", state->write_generation); +} +CONFIGFS_ATTR_RO(tsm_report_, generation); + +static ssize_t tsm_report_provider_show(struct config_item *cfg, char *buf) +{ + guard(rwsem_read)(&tsm_rwsem); + if (!provider.ops) + return -ENXIO; + + return sysfs_emit(buf, "%s\n", provider.ops->name); +} +CONFIGFS_ATTR_RO(tsm_report_, provider); + +static ssize_t __read_report(struct tsm_report *report, void *buf, size_t count, + enum tsm_data_select select) +{ + loff_t offset = 0; + ssize_t len; + u8 *out; + + if (select == TSM_REPORT) { + out = report->outblob; + len = report->outblob_len; + } else if (select == TSM_MANIFEST) { + out = report->manifestblob; + len = report->manifestblob_len; + } else { + out = report->auxblob; + len = report->auxblob_len; + } + + /* + * Recall that a NULL @buf is configfs requesting the size of + * the buffer. + */ + if (!buf) + return len; + return memory_read_from_buffer(buf, count, &offset, out, len); +} + +static ssize_t read_cached_report(struct tsm_report *report, void *buf, + size_t count, enum tsm_data_select select) +{ + struct tsm_report_state *state = to_state(report); + + guard(rwsem_read)(&tsm_rwsem); + if (!report->desc.inblob_len) + return -EINVAL; + + /* + * A given TSM backend always fills in ->outblob regardless of + * whether the report includes an auxblob/manifestblob or not. + */ + if (!report->outblob || + state->read_generation != state->write_generation) + return -EWOULDBLOCK; + + return __read_report(report, buf, count, select); +} + +static ssize_t tsm_report_read(struct tsm_report *report, void *buf, + size_t count, enum tsm_data_select select) +{ + struct tsm_report_state *state = to_state(report); + const struct tsm_report_ops *ops; + ssize_t rc; + + /* try to read from the existing report if present and valid... */ + rc = read_cached_report(report, buf, count, select); + if (rc >= 0 || rc != -EWOULDBLOCK) + return rc; + + /* slow path, report may need to be regenerated... */ + guard(rwsem_write)(&tsm_rwsem); + ops = provider.ops; + if (!ops) + return -ENXIO; + if (!report->desc.inblob_len) + return -EINVAL; + + /* did another thread already generate this report? */ + if (report->outblob && + state->read_generation == state->write_generation) + goto out; + + kvfree(report->outblob); + kvfree(report->auxblob); + kvfree(report->manifestblob); + report->outblob = NULL; + report->auxblob = NULL; + report->manifestblob = NULL; + rc = ops->report_new(report, provider.data); + if (rc < 0) + return rc; + state->read_generation = state->write_generation; +out: + return __read_report(report, buf, count, select); +} + +static ssize_t tsm_report_outblob_read(struct config_item *cfg, void *buf, + size_t count) +{ + struct tsm_report *report = to_tsm_report(cfg); + + return tsm_report_read(report, buf, count, TSM_REPORT); +} +CONFIGFS_BIN_ATTR_RO(tsm_report_, outblob, NULL, TSM_REPORT_OUTBLOB_MAX); + +static ssize_t tsm_report_auxblob_read(struct config_item *cfg, void *buf, + size_t count) +{ + struct tsm_report *report = to_tsm_report(cfg); + + return tsm_report_read(report, buf, count, TSM_CERTS); +} +CONFIGFS_BIN_ATTR_RO(tsm_report_, auxblob, NULL, TSM_REPORT_OUTBLOB_MAX); + +static ssize_t tsm_report_manifestblob_read(struct config_item *cfg, void *buf, + size_t count) +{ + struct tsm_report *report = to_tsm_report(cfg); + + return tsm_report_read(report, buf, count, TSM_MANIFEST); +} +CONFIGFS_BIN_ATTR_RO(tsm_report_, manifestblob, NULL, TSM_REPORT_OUTBLOB_MAX); + +static struct configfs_attribute *tsm_report_attrs[] = { + [TSM_REPORT_GENERATION] = &tsm_report_attr_generation, + [TSM_REPORT_PROVIDER] = &tsm_report_attr_provider, + [TSM_REPORT_PRIVLEVEL] = &tsm_report_attr_privlevel, + [TSM_REPORT_PRIVLEVEL_FLOOR] = &tsm_report_attr_privlevel_floor, + [TSM_REPORT_SERVICE_PROVIDER] = &tsm_report_attr_service_provider, + [TSM_REPORT_SERVICE_GUID] = &tsm_report_attr_service_guid, + [TSM_REPORT_SERVICE_MANIFEST_VER] = &tsm_report_attr_service_manifest_version, + NULL, +}; + +static struct configfs_bin_attribute *tsm_report_bin_attrs[] = { + [TSM_REPORT_INBLOB] = &tsm_report_attr_inblob, + [TSM_REPORT_OUTBLOB] = &tsm_report_attr_outblob, + [TSM_REPORT_AUXBLOB] = &tsm_report_attr_auxblob, + [TSM_REPORT_MANIFESTBLOB] = &tsm_report_attr_manifestblob, + NULL, +}; + +static void tsm_report_item_release(struct config_item *cfg) +{ + struct tsm_report *report = to_tsm_report(cfg); + struct tsm_report_state *state = to_state(report); + + kvfree(report->manifestblob); + kvfree(report->auxblob); + kvfree(report->outblob); + kfree(report->desc.service_provider); + kfree(state); +} + +static struct configfs_item_operations tsm_report_item_ops = { + .release = tsm_report_item_release, +}; + +static bool tsm_report_is_visible(struct config_item *item, + struct configfs_attribute *attr, int n) +{ + guard(rwsem_read)(&tsm_rwsem); + if (!provider.ops) + return false; + + if (!provider.ops->report_attr_visible) + return true; + + return provider.ops->report_attr_visible(n); +} + +static bool tsm_report_is_bin_visible(struct config_item *item, + struct configfs_bin_attribute *attr, int n) +{ + guard(rwsem_read)(&tsm_rwsem); + if (!provider.ops) + return false; + + if (!provider.ops->report_bin_attr_visible) + return true; + + return provider.ops->report_bin_attr_visible(n); +} + +static struct configfs_group_operations tsm_report_attr_group_ops = { + .is_visible = tsm_report_is_visible, + .is_bin_visible = tsm_report_is_bin_visible, +}; + +static const struct config_item_type tsm_report_type = { + .ct_owner = THIS_MODULE, + .ct_bin_attrs = tsm_report_bin_attrs, + .ct_attrs = tsm_report_attrs, + .ct_item_ops = &tsm_report_item_ops, + .ct_group_ops = &tsm_report_attr_group_ops, +}; + +static struct config_item *tsm_report_make_item(struct config_group *group, + const char *name) +{ + struct tsm_report_state *state; + + guard(rwsem_read)(&tsm_rwsem); + if (!provider.ops) + return ERR_PTR(-ENXIO); + + state = kzalloc_obj(*state); + if (!state) + return ERR_PTR(-ENOMEM); + + atomic_inc(&provider.count); + config_item_init_type_name(&state->cfg, name, &tsm_report_type); + return &state->cfg; +} + +static void tsm_report_drop_item(struct config_group *group, struct config_item *item) +{ + config_item_put(item); + atomic_dec(&provider.count); +} + +static struct configfs_group_operations tsm_report_group_ops = { + .make_item = tsm_report_make_item, + .drop_item = tsm_report_drop_item, +}; + +static const struct config_item_type tsm_reports_type = { + .ct_owner = THIS_MODULE, + .ct_group_ops = &tsm_report_group_ops, +}; + +static const struct config_item_type tsm_root_group_type = { + .ct_owner = THIS_MODULE, +}; + +static struct configfs_subsystem tsm_configfs = { + .su_group = { + .cg_item = { + .ci_namebuf = "tsm", + .ci_type = &tsm_root_group_type, + }, + }, + .su_mutex = __MUTEX_INITIALIZER(tsm_configfs.su_mutex), +}; + +int tsm_report_register(const struct tsm_report_ops *ops, void *priv) +{ + const struct tsm_report_ops *conflict; + + guard(rwsem_write)(&tsm_rwsem); + conflict = provider.ops; + if (conflict) { + pr_err("\"%s\" ops already registered\n", conflict->name); + return -EBUSY; + } + + if (atomic_read(&provider.count)) { + pr_err("configfs/tsm/report not empty\n"); + return -EBUSY; + } + + provider.ops = ops; + provider.data = priv; + return 0; +} +EXPORT_SYMBOL_GPL(tsm_report_register); + +int tsm_report_unregister(const struct tsm_report_ops *ops) +{ + guard(rwsem_write)(&tsm_rwsem); + if (ops != provider.ops) + return -EBUSY; + if (atomic_read(&provider.count)) + pr_warn("\"%s\" unregistered with items present in configfs/tsm/report\n", + provider.ops->name); + provider.ops = NULL; + provider.data = NULL; + return 0; +} +EXPORT_SYMBOL_GPL(tsm_report_unregister); + +static struct config_group *tsm_report_group; + +static int __init tsm_report_init(void) +{ + struct config_group *root = &tsm_configfs.su_group; + struct config_group *tsm; + int rc; + + config_group_init(root); + rc = configfs_register_subsystem(&tsm_configfs); + if (rc) + return rc; + + tsm = configfs_register_default_group(root, "report", + &tsm_reports_type); + if (IS_ERR(tsm)) { + configfs_unregister_subsystem(&tsm_configfs); + return PTR_ERR(tsm); + } + tsm_report_group = tsm; + + return 0; +} +module_init(tsm_report_init); + +static void __exit tsm_report_exit(void) +{ + configfs_unregister_default_group(tsm_report_group); + configfs_unregister_subsystem(&tsm_configfs); +} +module_exit(tsm_report_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Provide Trusted Security Module attestation reports via configfs"); diff --git a/drivers/virt/coco/guest/tsm-mr.c b/drivers/virt/coco/guest/tsm-mr.c new file mode 100644 index 000000000..657b9c573 --- /dev/null +++ b/drivers/virt/coco/guest/tsm-mr.c @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright(c) 2024-2025 Intel Corporation. All rights reserved. */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/sysfs.h> + +#define CREATE_TRACE_POINTS +#include <trace/events/tsm_mr.h> + +/* + * struct tm_context - contains everything necessary to implement sysfs + * attributes for MRs. + * @rwsem: protects the MR cache from concurrent access. + * @agrp: contains all MR attributes created by tsm_mr_create_attribute_group(). + * @tm: input to tsm_mr_create_attribute_group() containing MR definitions/ops. + * @in_sync: %true if MR cache is up-to-date. + * @mrs: array of &struct bin_attribute, one for each MR. + * + * This internal structure contains everything needed to implement + * tm_digest_read() and tm_digest_write(). + * + * Given tm->refresh() is potentially expensive, tm_digest_read() caches MR + * values and calls tm->refresh() only when necessary. Only live MRs (i.e., with + * %TSM_MR_F_LIVE set) can trigger tm->refresh(), while others are assumed to + * retain their values from the last tm->write(). @in_sync tracks if there have + * been tm->write() calls since the last tm->refresh(). That is, tm->refresh() + * will be called only when a live MR is being read and the cache is stale + * (@in_sync is %false). + * + * tm_digest_write() sets @in_sync to %false and calls tm->write(), whose + * semantics is arch and MR specific. Most (if not all) writable MRs support the + * extension semantics (i.e., tm->write() extends the input buffer into the MR). + */ +struct tm_context { + struct rw_semaphore rwsem; + struct attribute_group agrp; + const struct tsm_measurements *tm; + bool in_sync; + struct bin_attribute mrs[]; +}; + +static ssize_t tm_digest_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, char *buffer, + loff_t off, size_t count) +{ + struct tm_context *ctx; + const struct tsm_measurement_register *mr; + int rc; + + ctx = attr->private; + rc = down_read_interruptible(&ctx->rwsem); + if (rc) + return rc; + + mr = &ctx->tm->mrs[attr - ctx->mrs]; + + /* + * @ctx->in_sync indicates if the MR cache is stale. It is a global + * instead of a per-MR flag for simplicity, as most (if not all) archs + * allow reading all MRs in oneshot. + * + * ctx->refresh() is necessary only for LIVE MRs, while others retain + * their values from their respective last ctx->write(). + */ + if ((mr->mr_flags & TSM_MR_F_LIVE) && !ctx->in_sync) { + up_read(&ctx->rwsem); + + rc = down_write_killable(&ctx->rwsem); + if (rc) + return rc; + + if (!ctx->in_sync) { + rc = ctx->tm->refresh(ctx->tm); + ctx->in_sync = !rc; + trace_tsm_mr_refresh(mr, rc); + } + + downgrade_write(&ctx->rwsem); + } + + memcpy(buffer, mr->mr_value + off, count); + trace_tsm_mr_read(mr); + + up_read(&ctx->rwsem); + return rc ?: count; +} + +static ssize_t tm_digest_write(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, char *buffer, + loff_t off, size_t count) +{ + struct tm_context *ctx; + const struct tsm_measurement_register *mr; + ssize_t rc; + + /* partial writes are not supported */ + if (off != 0 || count != attr->size) + return -EINVAL; + + ctx = attr->private; + mr = &ctx->tm->mrs[attr - ctx->mrs]; + + rc = down_write_killable(&ctx->rwsem); + if (rc) + return rc; + + rc = ctx->tm->write(ctx->tm, mr, buffer); + + /* mark MR cache stale */ + if (!rc) { + ctx->in_sync = false; + trace_tsm_mr_write(mr, buffer); + } + + up_write(&ctx->rwsem); + return rc ?: count; +} + +/** + * tsm_mr_create_attribute_group() - creates an attribute group for measurement + * registers (MRs) + * @tm: pointer to &struct tsm_measurements containing the MR definitions. + * + * This function creates attributes corresponding to the MR definitions + * provided by @tm->mrs. + * + * The created attributes will reference @tm and its members. The caller must + * not free @tm until after tsm_mr_free_attribute_group() is called. + * + * Context: Process context. May sleep due to memory allocation. + * + * Return: + * * On success, the pointer to a an attribute group is returned; otherwise + * * %-EINVAL - Invalid MR definitions. + * * %-ENOMEM - Out of memory. + */ +const struct attribute_group * +tsm_mr_create_attribute_group(const struct tsm_measurements *tm) +{ + size_t nlen; + + if (!tm || !tm->mrs) + return ERR_PTR(-EINVAL); + + /* aggregated length of all MR names */ + nlen = 0; + for (size_t i = 0; i < tm->nr_mrs; ++i) { + if ((tm->mrs[i].mr_flags & TSM_MR_F_LIVE) && !tm->refresh) + return ERR_PTR(-EINVAL); + + if ((tm->mrs[i].mr_flags & TSM_MR_F_WRITABLE) && !tm->write) + return ERR_PTR(-EINVAL); + + if (!tm->mrs[i].mr_name) + return ERR_PTR(-EINVAL); + + if (tm->mrs[i].mr_flags & TSM_MR_F_NOHASH) + continue; + + if (tm->mrs[i].mr_hash >= HASH_ALGO__LAST) + return ERR_PTR(-EINVAL); + + /* MR sysfs attribute names have the form of MRNAME:HASH */ + nlen += strlen(tm->mrs[i].mr_name) + 1 + + strlen(hash_algo_name[tm->mrs[i].mr_hash]) + 1; + } + + /* + * @attrs and the MR name strings are combined into a single allocation + * so that we don't have to free MR names one-by-one in + * tsm_mr_free_attribute_group() + */ + const struct bin_attribute **attrs __free(kfree) = + kzalloc(sizeof(*attrs) * (tm->nr_mrs + 1) + nlen, GFP_KERNEL); + struct tm_context *ctx __free(kfree) = + kzalloc_flex(*ctx, mrs, tm->nr_mrs); + char *name, *end; + + if (!ctx || !attrs) + return ERR_PTR(-ENOMEM); + + /* @attrs is followed immediately by MR name strings */ + name = (char *)&attrs[tm->nr_mrs + 1]; + end = name + nlen; + + for (size_t i = 0; i < tm->nr_mrs; ++i) { + struct bin_attribute *bap = &ctx->mrs[i]; + + sysfs_bin_attr_init(bap); + + if (tm->mrs[i].mr_flags & TSM_MR_F_NOHASH) + bap->attr.name = tm->mrs[i].mr_name; + else if (name < end) { + bap->attr.name = name; + name += snprintf(name, end - name, "%s:%s", + tm->mrs[i].mr_name, + hash_algo_name[tm->mrs[i].mr_hash]); + ++name; + } else + return ERR_PTR(-EINVAL); + + /* check for duplicated MR definitions */ + for (size_t j = 0; j < i; ++j) + if (!strcmp(bap->attr.name, attrs[j]->attr.name)) + return ERR_PTR(-EINVAL); + + if (tm->mrs[i].mr_flags & TSM_MR_F_READABLE) { + bap->attr.mode |= 0444; + bap->read = tm_digest_read; + } + + if (tm->mrs[i].mr_flags & TSM_MR_F_WRITABLE) { + bap->attr.mode |= 0200; + bap->write = tm_digest_write; + } + + bap->size = tm->mrs[i].mr_size; + bap->private = ctx; + + attrs[i] = bap; + } + + if (name != end) + return ERR_PTR(-EINVAL); + + init_rwsem(&ctx->rwsem); + ctx->agrp.name = "measurements"; + ctx->agrp.bin_attrs = no_free_ptr(attrs); + ctx->tm = tm; + return &no_free_ptr(ctx)->agrp; +} +EXPORT_SYMBOL_GPL(tsm_mr_create_attribute_group); + +/** + * tsm_mr_free_attribute_group() - frees the attribute group returned by + * tsm_mr_create_attribute_group() + * @attr_grp: attribute group returned by tsm_mr_create_attribute_group() + * + * Context: Process context. + */ +void tsm_mr_free_attribute_group(const struct attribute_group *attr_grp) +{ + if (!IS_ERR_OR_NULL(attr_grp)) { + kfree(attr_grp->bin_attrs); + kfree(container_of(attr_grp, struct tm_context, agrp)); + } +} +EXPORT_SYMBOL_GPL(tsm_mr_free_attribute_group); diff --git a/drivers/virt/coco/pkvm-guest/Kconfig b/drivers/virt/coco/pkvm-guest/Kconfig new file mode 100644 index 000000000..928b8e166 --- /dev/null +++ b/drivers/virt/coco/pkvm-guest/Kconfig @@ -0,0 +1,10 @@ +config ARM_PKVM_GUEST + bool "Arm pKVM protected guest driver" + depends on ARM64 && DMA_RESTRICTED_POOL + help + Protected guests running under the pKVM hypervisor on arm64 + are isolated from the host and must issue hypercalls to enable + interaction with virtual devices. This driver implements + support for probing and issuing these hypercalls. + + If unsure, say 'N'. diff --git a/drivers/virt/coco/pkvm-guest/Makefile b/drivers/virt/coco/pkvm-guest/Makefile new file mode 100644 index 000000000..4bee24579 --- /dev/null +++ b/drivers/virt/coco/pkvm-guest/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +obj-$(CONFIG_ARM_PKVM_GUEST) += arm-pkvm-guest.o diff --git a/drivers/virt/coco/pkvm-guest/arm-pkvm-guest.c b/drivers/virt/coco/pkvm-guest/arm-pkvm-guest.c new file mode 100644 index 000000000..26fe9c3f2 --- /dev/null +++ b/drivers/virt/coco/pkvm-guest/arm-pkvm-guest.c @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Support for the hypercall interface exposed to protected guests by + * pKVM. + * + * Author: Will Deacon <will@kernel.org> + * Copyright (C) 2024 Google LLC + */ + +#include <linux/arm-smccc.h> +#include <linux/array_size.h> +#include <linux/io.h> +#include <linux/mem_encrypt.h> +#include <linux/mm.h> +#include <linux/pgtable.h> + +#include <asm/hypervisor.h> + +static size_t pkvm_granule; +DEFINE_STATIC_KEY_FALSE_RO(pkvm_guest); + +static int arm_smccc_do_one_page(u32 func_id, phys_addr_t phys) +{ + phys_addr_t end = phys + PAGE_SIZE; + + while (phys < end) { + struct arm_smccc_res res; + + arm_smccc_1_1_invoke(func_id, phys, 0, 0, &res); + if (res.a0 != SMCCC_RET_SUCCESS) + return -EPERM; + + phys += pkvm_granule; + } + + return 0; +} + +static int __set_memory_range(u32 func_id, unsigned long start, int numpages) +{ + void *addr = (void *)start, *end = addr + numpages * PAGE_SIZE; + + while (addr < end) { + int err; + + err = arm_smccc_do_one_page(func_id, virt_to_phys(addr)); + if (err) + return err; + + addr += PAGE_SIZE; + } + + return 0; +} + +static int pkvm_set_memory_encrypted(unsigned long addr, int numpages) +{ + return __set_memory_range(ARM_SMCCC_VENDOR_HYP_KVM_MEM_UNSHARE_FUNC_ID, + addr, numpages); +} + +static int pkvm_set_memory_decrypted(unsigned long addr, int numpages) +{ + return __set_memory_range(ARM_SMCCC_VENDOR_HYP_KVM_MEM_SHARE_FUNC_ID, + addr, numpages); +} + +static const struct arm64_mem_crypt_ops pkvm_crypt_ops = { + .encrypt = pkvm_set_memory_encrypted, + .decrypt = pkvm_set_memory_decrypted, +}; + +static int mmio_guard_ioremap_hook(phys_addr_t phys, size_t size, + pgprot_t *prot) +{ + phys_addr_t end; + pteval_t protval = pgprot_val(*prot); + + /* + * We only expect MMIO emulation for regions mapped with device + * attributes. + */ + if (protval != PROT_DEVICE_nGnRE && protval != PROT_DEVICE_nGnRnE) + return 0; + + end = PAGE_ALIGN(phys + size); + phys = PAGE_ALIGN_DOWN(phys); + + while (phys < end) { + const int func_id = ARM_SMCCC_VENDOR_HYP_KVM_MMIO_GUARD_FUNC_ID; + + WARN_ON_ONCE(arm_smccc_do_one_page(func_id, phys)); + phys += PAGE_SIZE; + } + + return 0; +} + +void pkvm_init_hyp_services(void) +{ + int i; + struct arm_smccc_res res; + const u32 funcs[] = { + ARM_SMCCC_KVM_FUNC_HYP_MEMINFO, + ARM_SMCCC_KVM_FUNC_MEM_SHARE, + ARM_SMCCC_KVM_FUNC_MEM_UNSHARE, + }; + + for (i = 0; i < ARRAY_SIZE(funcs); ++i) { + if (!kvm_arm_hyp_service_available(funcs[i])) + return; + } + + arm_smccc_1_1_invoke(ARM_SMCCC_VENDOR_HYP_KVM_HYP_MEMINFO_FUNC_ID, + 0, 0, 0, &res); + if (res.a0 > PAGE_SIZE) /* Includes error codes */ + return; + + pkvm_granule = res.a0; + arm64_mem_crypt_ops_register(&pkvm_crypt_ops); + + if (kvm_arm_hyp_service_available(ARM_SMCCC_KVM_FUNC_MMIO_GUARD)) + arm64_ioremap_prot_hook_register(&mmio_guard_ioremap_hook); + + static_branch_enable(&pkvm_guest); +} diff --git a/drivers/virt/coco/sev-guest/Kconfig b/drivers/virt/coco/sev-guest/Kconfig new file mode 100644 index 000000000..a6405ab6c --- /dev/null +++ b/drivers/virt/coco/sev-guest/Kconfig @@ -0,0 +1,13 @@ +config SEV_GUEST + tristate "AMD SEV Guest driver" + default m + depends on AMD_MEM_ENCRYPT + select TSM_REPORTS + help + SEV-SNP firmware provides the guest a mechanism to communicate with + the PSP without risk from a malicious hypervisor who wishes to read, + alter, drop or replay the messages sent. The driver provides + userspace interface to communicate with the PSP to request the + attestation report and more. + + If you choose 'M' here, this module will be called sev-guest. diff --git a/drivers/virt/coco/sev-guest/Makefile b/drivers/virt/coco/sev-guest/Makefile new file mode 100644 index 000000000..63d67c277 --- /dev/null +++ b/drivers/virt/coco/sev-guest/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +obj-$(CONFIG_SEV_GUEST) += sev-guest.o diff --git a/drivers/virt/coco/sev-guest/sev-guest.c b/drivers/virt/coco/sev-guest/sev-guest.c new file mode 100644 index 000000000..935537a41 --- /dev/null +++ b/drivers/virt/coco/sev-guest/sev-guest.c @@ -0,0 +1,716 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * AMD Secure Encrypted Virtualization (SEV) guest driver interface + * + * Copyright (C) 2021-2024 Advanced Micro Devices, Inc. + * + * Author: Brijesh Singh <brijesh.singh@amd.com> + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/types.h> +#include <linux/mutex.h> +#include <linux/io.h> +#include <linux/platform_device.h> +#include <linux/miscdevice.h> +#include <linux/set_memory.h> +#include <linux/fs.h> +#include <linux/tsm.h> +#include <linux/psp-sev.h> +#include <linux/sockptr.h> +#include <linux/cleanup.h> +#include <linux/uuid.h> +#include <linux/configfs.h> +#include <linux/mm.h> +#include <uapi/linux/sev-guest.h> +#include <uapi/linux/psp-sev.h> + +#include <asm/svm.h> +#include <asm/sev.h> + +#define DEVICE_NAME "sev-guest" + +#define SVSM_MAX_RETRIES 3 + +struct snp_guest_dev { + struct device *dev; + struct miscdevice misc; + + struct snp_msg_desc *msg_desc; +}; + +/* + * The VMPCK ID represents the key used by the SNP guest to communicate with the + * SEV firmware in the AMD Secure Processor (ASP, aka PSP). By default, the key + * used will be the key associated with the VMPL at which the guest is running. + * Should the default key be wiped (see snp_disable_vmpck()), this parameter + * allows for using one of the remaining VMPCKs. + */ +static int vmpck_id = -1; +module_param(vmpck_id, int, 0444); +MODULE_PARM_DESC(vmpck_id, "The VMPCK ID to use when communicating with the PSP."); + +static inline struct snp_guest_dev *to_snp_dev(struct file *file) +{ + struct miscdevice *dev = file->private_data; + + return container_of(dev, struct snp_guest_dev, misc); +} + +struct snp_req_resp { + sockptr_t req_data; + sockptr_t resp_data; +}; + +static int get_report(struct snp_guest_dev *snp_dev, struct snp_guest_request_ioctl *arg) +{ + struct snp_report_req *report_req __free(kfree) = NULL; + struct snp_msg_desc *mdesc = snp_dev->msg_desc; + struct snp_report_resp *report_resp; + struct snp_guest_req req = {}; + int rc, resp_len; + + if (!arg->req_data || !arg->resp_data) + return -EINVAL; + + report_req = kzalloc_obj(*report_req, GFP_KERNEL_ACCOUNT); + if (!report_req) + return -ENOMEM; + + if (copy_from_user(report_req, (void __user *)arg->req_data, sizeof(*report_req))) + return -EFAULT; + + /* + * The intermediate response buffer is used while decrypting the + * response payload. Make sure that it has enough space to cover the + * authtag. + */ + resp_len = sizeof(report_resp->data) + AUTHTAG_LEN; + report_resp = kzalloc(resp_len, GFP_KERNEL_ACCOUNT); + if (!report_resp) + return -ENOMEM; + + req.msg_version = arg->msg_version; + req.msg_type = SNP_MSG_REPORT_REQ; + req.vmpck_id = mdesc->vmpck_id; + req.req_buf = report_req; + req.req_sz = sizeof(*report_req); + req.resp_buf = report_resp->data; + req.resp_sz = resp_len; + req.exit_code = SVM_VMGEXIT_GUEST_REQUEST; + + rc = snp_send_guest_request(mdesc, &req); + arg->exitinfo2 = req.exitinfo2; + if (rc) + goto e_free; + + if (copy_to_user((void __user *)arg->resp_data, report_resp, sizeof(*report_resp))) + rc = -EFAULT; + +e_free: + kfree(report_resp); + return rc; +} + +static int get_derived_key(struct snp_guest_dev *snp_dev, struct snp_guest_request_ioctl *arg) +{ + struct snp_derived_key_resp *derived_key_resp __free(kfree) = NULL; + struct snp_derived_key_req *derived_key_req __free(kfree) = NULL; + struct snp_msg_desc *mdesc = snp_dev->msg_desc; + struct snp_guest_req req = {}; + int rc, resp_len; + + if (!arg->req_data || !arg->resp_data) + return -EINVAL; + + /* + * The intermediate response buffer is used while decrypting the + * response payload. Make sure that it has enough space to cover the + * authtag. + */ + resp_len = sizeof(derived_key_resp->data) + AUTHTAG_LEN; + derived_key_resp = kzalloc(resp_len, GFP_KERNEL_ACCOUNT); + if (!derived_key_resp) + return -ENOMEM; + + derived_key_req = kzalloc_obj(*derived_key_req, GFP_KERNEL_ACCOUNT); + if (!derived_key_req) + return -ENOMEM; + + if (copy_from_user(derived_key_req, (void __user *)arg->req_data, + sizeof(*derived_key_req))) + return -EFAULT; + + req.msg_version = arg->msg_version; + req.msg_type = SNP_MSG_KEY_REQ; + req.vmpck_id = mdesc->vmpck_id; + req.req_buf = derived_key_req; + req.req_sz = sizeof(*derived_key_req); + req.resp_buf = derived_key_resp; + req.resp_sz = resp_len; + req.exit_code = SVM_VMGEXIT_GUEST_REQUEST; + + rc = snp_send_guest_request(mdesc, &req); + arg->exitinfo2 = req.exitinfo2; + if (!rc) { + if (copy_to_user((void __user *)arg->resp_data, derived_key_resp, + sizeof(derived_key_resp->data))) + rc = -EFAULT; + } + + /* The response buffer contains the sensitive data, explicitly clear it. */ + memzero_explicit(derived_key_resp, sizeof(*derived_key_resp)); + + return rc; +} + +static int get_ext_report(struct snp_guest_dev *snp_dev, struct snp_guest_request_ioctl *arg, + struct snp_req_resp *io) + +{ + struct snp_ext_report_req *report_req __free(kfree) = NULL; + struct snp_msg_desc *mdesc = snp_dev->msg_desc; + struct snp_report_resp *report_resp; + struct snp_guest_req req = {}; + int ret, npages = 0, resp_len; + sockptr_t certs_address; + u64 pfn; + + if (sockptr_is_null(io->req_data) || sockptr_is_null(io->resp_data)) + return -EINVAL; + + report_req = kzalloc_obj(*report_req, GFP_KERNEL_ACCOUNT); + if (!report_req) + return -ENOMEM; + + if (copy_from_sockptr(report_req, io->req_data, sizeof(*report_req))) + return -EFAULT; + + /* caller does not want certificate data */ + if (!report_req->certs_len || !report_req->certs_address) + goto cmd; + + if (report_req->certs_len > SEV_FW_BLOB_MAX_SIZE || + !IS_ALIGNED(report_req->certs_len, PAGE_SIZE)) + return -EINVAL; + + if (sockptr_is_kernel(io->resp_data)) { + certs_address = KERNEL_SOCKPTR((void *)report_req->certs_address); + } else { + certs_address = USER_SOCKPTR((void __user *)report_req->certs_address); + if (!access_ok(certs_address.user, report_req->certs_len)) + return -EFAULT; + } + + /* + * Initialize the intermediate buffer with all zeros. This buffer + * is used in the guest request message to get the certs blob from + * the host. If host does not supply any certs in it, then copy + * zeros to indicate that certificate data was not provided. + */ + npages = report_req->certs_len >> PAGE_SHIFT; + req.certs_data = alloc_pages_exact(npages << PAGE_SHIFT, + GFP_KERNEL_ACCOUNT | __GFP_ZERO); + if (!req.certs_data) + return -ENOMEM; + + pfn = PHYS_PFN(virt_to_phys(req.certs_data)); + ret = set_memory_decrypted((unsigned long)req.certs_data, npages); + if (ret) { + pr_err("failed to mark page shared, ret=%d\n", ret); + snp_leak_pages(pfn, npages); + return -EFAULT; + } + +cmd: + /* + * The intermediate response buffer is used while decrypting the + * response payload. Make sure that it has enough space to cover the + * authtag. + */ + resp_len = sizeof(report_resp->data) + AUTHTAG_LEN; + report_resp = kzalloc(resp_len, GFP_KERNEL_ACCOUNT); + if (!report_resp) { + ret = -ENOMEM; + goto e_free_data; + } + + req.input.data_npages = npages; + + req.msg_version = arg->msg_version; + req.msg_type = SNP_MSG_REPORT_REQ; + req.vmpck_id = mdesc->vmpck_id; + req.req_buf = &report_req->data; + req.req_sz = sizeof(report_req->data); + req.resp_buf = report_resp->data; + req.resp_sz = resp_len; + req.exit_code = SVM_VMGEXIT_EXT_GUEST_REQUEST; + + ret = snp_send_guest_request(mdesc, &req); + arg->exitinfo2 = req.exitinfo2; + + /* If certs length is invalid then copy the returned length */ + if (arg->vmm_error == SNP_GUEST_VMM_ERR_INVALID_LEN) { + report_req->certs_len = req.input.data_npages << PAGE_SHIFT; + + if (copy_to_sockptr(io->req_data, report_req, sizeof(*report_req))) + ret = -EFAULT; + } + + if (ret) + goto e_free; + + if (npages && copy_to_sockptr(certs_address, req.certs_data, report_req->certs_len)) { + ret = -EFAULT; + goto e_free; + } + + if (copy_to_sockptr(io->resp_data, report_resp, sizeof(*report_resp))) + ret = -EFAULT; + +e_free: + kfree(report_resp); +e_free_data: + if (npages) { + if (set_memory_encrypted((unsigned long)req.certs_data, npages)) { + WARN_ONCE(ret, "failed to restore encryption mask (leak it)\n"); + snp_leak_pages(pfn, npages); + } else { + free_pages_exact(req.certs_data, npages << PAGE_SHIFT); + } + } + return ret; +} + +static long snp_guest_ioctl(struct file *file, unsigned int ioctl, unsigned long arg) +{ + struct snp_guest_dev *snp_dev = to_snp_dev(file); + void __user *argp = (void __user *)arg; + struct snp_guest_request_ioctl input; + struct snp_req_resp io; + int ret = -ENOTTY; + + if (copy_from_user(&input, argp, sizeof(input))) + return -EFAULT; + + input.exitinfo2 = 0xff; + + /* Message version must be non-zero */ + if (!input.msg_version) + return -EINVAL; + + switch (ioctl) { + case SNP_GET_REPORT: + ret = get_report(snp_dev, &input); + break; + case SNP_GET_DERIVED_KEY: + ret = get_derived_key(snp_dev, &input); + break; + case SNP_GET_EXT_REPORT: + /* + * As get_ext_report() may be called from the ioctl() path and a + * kernel internal path (configfs-tsm), decorate the passed + * buffers as user pointers. + */ + io.req_data = USER_SOCKPTR((void __user *)input.req_data); + io.resp_data = USER_SOCKPTR((void __user *)input.resp_data); + ret = get_ext_report(snp_dev, &input, &io); + break; + default: + break; + } + + if (input.exitinfo2 && copy_to_user(argp, &input, sizeof(input))) + return -EFAULT; + + return ret; +} + +static const struct file_operations snp_guest_fops = { + .owner = THIS_MODULE, + .unlocked_ioctl = snp_guest_ioctl, +}; + +struct snp_msg_report_resp_hdr { + u32 status; + u32 report_size; + u8 rsvd[24]; +}; + +struct snp_msg_cert_entry { + guid_t guid; + u32 offset; + u32 length; +}; + +static int sev_svsm_report_new(struct tsm_report *report, void *data) +{ + unsigned int rep_len, man_len, certs_len; + struct tsm_report_desc *desc = &report->desc; + struct svsm_attest_call ac = {}; + unsigned int retry_count; + void *rep, *man, *certs; + struct svsm_call call; + unsigned int size; + bool try_again; + void *buffer; + u64 call_id; + int ret; + + /* + * Allocate pages for the request: + * - Report blob (4K) + * - Manifest blob (4K) + * - Certificate blob (16K) + * + * Above addresses must be 4K aligned + */ + rep_len = SZ_4K; + man_len = SZ_4K; + certs_len = SEV_FW_BLOB_MAX_SIZE; + + if (guid_is_null(&desc->service_guid)) { + call_id = SVSM_ATTEST_CALL(SVSM_ATTEST_SERVICES); + } else { + export_guid(ac.service_guid, &desc->service_guid); + ac.service_manifest_ver = desc->service_manifest_version; + + call_id = SVSM_ATTEST_CALL(SVSM_ATTEST_SINGLE_SERVICE); + } + + retry_count = 0; + +retry: + memset(&call, 0, sizeof(call)); + + size = rep_len + man_len + certs_len; + buffer = alloc_pages_exact(size, __GFP_ZERO); + if (!buffer) + return -ENOMEM; + + rep = buffer; + ac.report_buf.pa = __pa(rep); + ac.report_buf.len = rep_len; + + man = rep + rep_len; + ac.manifest_buf.pa = __pa(man); + ac.manifest_buf.len = man_len; + + certs = man + man_len; + ac.certificates_buf.pa = __pa(certs); + ac.certificates_buf.len = certs_len; + + ac.nonce.pa = __pa(desc->inblob); + ac.nonce.len = desc->inblob_len; + + ret = snp_issue_svsm_attest_req(call_id, &call, &ac); + if (ret) { + free_pages_exact(buffer, size); + + switch (call.rax_out) { + case SVSM_ERR_INVALID_PARAMETER: + try_again = false; + + if (ac.report_buf.len > rep_len) { + rep_len = PAGE_ALIGN(ac.report_buf.len); + try_again = true; + } + + if (ac.manifest_buf.len > man_len) { + man_len = PAGE_ALIGN(ac.manifest_buf.len); + try_again = true; + } + + if (ac.certificates_buf.len > certs_len) { + certs_len = PAGE_ALIGN(ac.certificates_buf.len); + try_again = true; + } + + /* If one of the buffers wasn't large enough, retry the request */ + if (try_again && retry_count < SVSM_MAX_RETRIES) { + retry_count++; + goto retry; + } + + return -EINVAL; + default: + pr_err_ratelimited("SVSM attestation request failed (%d / 0x%llx)\n", + ret, call.rax_out); + return -EINVAL; + } + } + + /* + * Allocate all the blob memory buffers at once so that the cleanup is + * done for errors that occur after the first allocation (i.e. before + * using no_free_ptr()). + */ + rep_len = ac.report_buf.len; + void *rbuf __free(kvfree) = kvzalloc(rep_len, GFP_KERNEL); + + man_len = ac.manifest_buf.len; + void *mbuf __free(kvfree) = kvzalloc(man_len, GFP_KERNEL); + + certs_len = ac.certificates_buf.len; + void *cbuf __free(kvfree) = certs_len ? kvzalloc(certs_len, GFP_KERNEL) : NULL; + + if (!rbuf || !mbuf || (certs_len && !cbuf)) { + free_pages_exact(buffer, size); + return -ENOMEM; + } + + memcpy(rbuf, rep, rep_len); + report->outblob = no_free_ptr(rbuf); + report->outblob_len = rep_len; + + memcpy(mbuf, man, man_len); + report->manifestblob = no_free_ptr(mbuf); + report->manifestblob_len = man_len; + + if (certs_len) { + memcpy(cbuf, certs, certs_len); + report->auxblob = no_free_ptr(cbuf); + report->auxblob_len = certs_len; + } + + free_pages_exact(buffer, size); + + return 0; +} + +static int sev_report_new(struct tsm_report *report, void *data) +{ + struct snp_msg_cert_entry *cert_table; + struct tsm_report_desc *desc = &report->desc; + struct snp_guest_dev *snp_dev = data; + struct snp_msg_report_resp_hdr hdr; + const u32 report_size = SZ_4K; + const u32 ext_size = SEV_FW_BLOB_MAX_SIZE; + u32 certs_size, i, size = report_size + ext_size; + int ret; + + if (desc->inblob_len != SNP_REPORT_USER_DATA_SIZE) + return -EINVAL; + + if (desc->service_provider) { + if (strcmp(desc->service_provider, "svsm")) + return -EINVAL; + + return sev_svsm_report_new(report, data); + } + + void *buf __free(kvfree) = kvzalloc(size, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + cert_table = buf + report_size; + struct snp_ext_report_req ext_req = { + .data = { .vmpl = desc->privlevel }, + .certs_address = (__u64)cert_table, + .certs_len = ext_size, + }; + memcpy(&ext_req.data.user_data, desc->inblob, desc->inblob_len); + + struct snp_guest_request_ioctl input = { + .msg_version = 1, + .req_data = (__u64)&ext_req, + .resp_data = (__u64)buf, + .exitinfo2 = 0xff, + }; + struct snp_req_resp io = { + .req_data = KERNEL_SOCKPTR(&ext_req), + .resp_data = KERNEL_SOCKPTR(buf), + }; + + ret = get_ext_report(snp_dev, &input, &io); + if (ret) + return ret; + + memcpy(&hdr, buf, sizeof(hdr)); + if (hdr.status == SEV_RET_INVALID_PARAM) + return -EINVAL; + if (hdr.status == SEV_RET_INVALID_KEY) + return -EINVAL; + if (hdr.status) + return -ENXIO; + if ((hdr.report_size + sizeof(hdr)) > report_size) + return -ENOMEM; + + void *rbuf __free(kvfree) = kvzalloc(hdr.report_size, GFP_KERNEL); + if (!rbuf) + return -ENOMEM; + + memcpy(rbuf, buf + sizeof(hdr), hdr.report_size); + report->outblob = no_free_ptr(rbuf); + report->outblob_len = hdr.report_size; + + certs_size = 0; + for (i = 0; i < ext_size / sizeof(struct snp_msg_cert_entry); i++) { + struct snp_msg_cert_entry *ent = &cert_table[i]; + + if (guid_is_null(&ent->guid) && !ent->offset && !ent->length) + break; + certs_size = max(certs_size, ent->offset + ent->length); + } + + /* Suspicious that the response populated entries without populating size */ + if (!certs_size && i) + dev_warn_ratelimited(snp_dev->dev, "certificate slots conveyed without size\n"); + + /* No certs to report */ + if (!certs_size) + return 0; + + /* Suspicious that the certificate blob size contract was violated + */ + if (certs_size > ext_size) { + dev_warn_ratelimited(snp_dev->dev, "certificate data truncated\n"); + certs_size = ext_size; + } + + void *cbuf __free(kvfree) = kvzalloc(certs_size, GFP_KERNEL); + if (!cbuf) + return -ENOMEM; + + memcpy(cbuf, cert_table, certs_size); + report->auxblob = no_free_ptr(cbuf); + report->auxblob_len = certs_size; + + return 0; +} + +static bool sev_report_attr_visible(int n) +{ + switch (n) { + case TSM_REPORT_GENERATION: + case TSM_REPORT_PROVIDER: + case TSM_REPORT_PRIVLEVEL: + case TSM_REPORT_PRIVLEVEL_FLOOR: + return true; + case TSM_REPORT_SERVICE_PROVIDER: + case TSM_REPORT_SERVICE_GUID: + case TSM_REPORT_SERVICE_MANIFEST_VER: + return snp_vmpl; + } + + return false; +} + +static bool sev_report_bin_attr_visible(int n) +{ + switch (n) { + case TSM_REPORT_INBLOB: + case TSM_REPORT_OUTBLOB: + case TSM_REPORT_AUXBLOB: + return true; + case TSM_REPORT_MANIFESTBLOB: + return snp_vmpl; + } + + return false; +} + +static struct tsm_report_ops sev_tsm_report_ops = { + .name = KBUILD_MODNAME, + .report_new = sev_report_new, + .report_attr_visible = sev_report_attr_visible, + .report_bin_attr_visible = sev_report_bin_attr_visible, +}; + +static void unregister_sev_tsm(void *data) +{ + tsm_report_unregister(&sev_tsm_report_ops); +} + +static int __init sev_guest_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct snp_guest_dev *snp_dev; + struct snp_msg_desc *mdesc; + struct miscdevice *misc; + int ret; + + BUILD_BUG_ON(sizeof(struct snp_guest_msg) > PAGE_SIZE); + + if (!cc_platform_has(CC_ATTR_GUEST_SEV_SNP)) + return -ENODEV; + + snp_dev = devm_kzalloc(&pdev->dev, sizeof(struct snp_guest_dev), GFP_KERNEL); + if (!snp_dev) + return -ENOMEM; + + mdesc = snp_msg_alloc(); + if (IS_ERR_OR_NULL(mdesc)) + return -ENOMEM; + + ret = snp_msg_init(mdesc, vmpck_id); + if (ret) + goto e_msg_init; + + platform_set_drvdata(pdev, snp_dev); + snp_dev->dev = dev; + + misc = &snp_dev->misc; + misc->minor = MISC_DYNAMIC_MINOR; + misc->name = DEVICE_NAME; + misc->fops = &snp_guest_fops; + + /* Set the privlevel_floor attribute based on the vmpck_id */ + sev_tsm_report_ops.privlevel_floor = mdesc->vmpck_id; + + ret = tsm_report_register(&sev_tsm_report_ops, snp_dev); + if (ret) + goto e_msg_init; + + ret = devm_add_action_or_reset(&pdev->dev, unregister_sev_tsm, NULL); + if (ret) + goto e_msg_init; + + ret = misc_register(misc); + if (ret) + goto e_msg_init; + + snp_dev->msg_desc = mdesc; + dev_info(dev, "Initialized SEV guest driver (using VMPCK%d communication key)\n", + mdesc->vmpck_id); + return 0; + +e_msg_init: + snp_msg_free(mdesc); + + return ret; +} + +static void __exit sev_guest_remove(struct platform_device *pdev) +{ + struct snp_guest_dev *snp_dev = platform_get_drvdata(pdev); + + snp_msg_free(snp_dev->msg_desc); + misc_deregister(&snp_dev->misc); +} + +/* + * This driver is meant to be a common SEV guest interface driver and to + * support any SEV guest API. As such, even though it has been introduced + * with the SEV-SNP support, it is named "sev-guest". + * + * sev_guest_remove() lives in .exit.text. For drivers registered via + * module_platform_driver_probe() this is ok because they cannot get unbound + * at runtime. So mark the driver struct with __refdata to prevent modpost + * triggering a section mismatch warning. + */ +static struct platform_driver sev_guest_driver __refdata = { + .remove = __exit_p(sev_guest_remove), + .driver = { + .name = "sev-guest", + }, +}; + +module_platform_driver_probe(sev_guest_driver, sev_guest_probe); + +MODULE_AUTHOR("Brijesh Singh <brijesh.singh@amd.com>"); +MODULE_LICENSE("GPL"); +MODULE_VERSION("1.0.0"); +MODULE_DESCRIPTION("AMD SEV Guest Driver"); +MODULE_ALIAS("platform:sev-guest"); diff --git a/drivers/virt/coco/tdx-guest/Kconfig b/drivers/virt/coco/tdx-guest/Kconfig new file mode 100644 index 000000000..dbbdc1438 --- /dev/null +++ b/drivers/virt/coco/tdx-guest/Kconfig @@ -0,0 +1,12 @@ +config TDX_GUEST_DRIVER + tristate "TDX Guest driver" + depends on INTEL_TDX_GUEST + select TSM_REPORTS + select TSM_MEASUREMENTS + help + The driver provides userspace interface to communicate with + the TDX module to request the TDX guest details like attestation + report. + + To compile this driver as module, choose M here. The module will + be called tdx-guest. diff --git a/drivers/virt/coco/tdx-guest/Makefile b/drivers/virt/coco/tdx-guest/Makefile new file mode 100644 index 000000000..775cb463f --- /dev/null +++ b/drivers/virt/coco/tdx-guest/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_TDX_GUEST_DRIVER) += tdx-guest.o diff --git a/drivers/virt/coco/tdx-guest/tdx-guest.c b/drivers/virt/coco/tdx-guest/tdx-guest.c new file mode 100644 index 000000000..d0303e31e --- /dev/null +++ b/drivers/virt/coco/tdx-guest/tdx-guest.c @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * TDX guest user interface driver + * + * Copyright (C) 2022 Intel Corporation + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/kernel.h> +#include <linux/miscdevice.h> +#include <linux/mm.h> +#include <linux/module.h> +#include <linux/string.h> +#include <linux/uaccess.h> +#include <linux/set_memory.h> +#include <linux/io.h> +#include <linux/delay.h> +#include <linux/sockptr.h> +#include <linux/tsm.h> +#include <linux/tsm-mr.h> + +#include <uapi/linux/tdx-guest.h> + +#include <asm/cpu_device_id.h> +#include <asm/tdx.h> + +/* TDREPORT buffer */ +static u8 *tdx_report_buf; + +/* Lock to serialize TDG.MR.REPORT and TDG.MR.RTMR.EXTEND TDCALLs */ +static DEFINE_MUTEX(mr_lock); + +/* TDREPORT fields */ +enum { + TDREPORT_reportdata = 128, + TDREPORT_tee_tcb_info = 256, + TDREPORT_tdinfo = TDREPORT_tee_tcb_info + 256, + TDREPORT_attributes = TDREPORT_tdinfo, + TDREPORT_xfam = TDREPORT_attributes + sizeof(u64), + TDREPORT_mrtd = TDREPORT_xfam + sizeof(u64), + TDREPORT_mrconfigid = TDREPORT_mrtd + SHA384_DIGEST_SIZE, + TDREPORT_mrowner = TDREPORT_mrconfigid + SHA384_DIGEST_SIZE, + TDREPORT_mrownerconfig = TDREPORT_mrowner + SHA384_DIGEST_SIZE, + TDREPORT_rtmr0 = TDREPORT_mrownerconfig + SHA384_DIGEST_SIZE, + TDREPORT_rtmr1 = TDREPORT_rtmr0 + SHA384_DIGEST_SIZE, + TDREPORT_rtmr2 = TDREPORT_rtmr1 + SHA384_DIGEST_SIZE, + TDREPORT_rtmr3 = TDREPORT_rtmr2 + SHA384_DIGEST_SIZE, + TDREPORT_servtd_hash = TDREPORT_rtmr3 + SHA384_DIGEST_SIZE, +}; + +static int tdx_do_report(sockptr_t data, sockptr_t tdreport) +{ + scoped_cond_guard(mutex_intr, return -EINTR, &mr_lock) { + u8 *reportdata = tdx_report_buf + TDREPORT_reportdata; + int ret; + + if (!sockptr_is_null(data) && + copy_from_sockptr(reportdata, data, TDX_REPORTDATA_LEN)) + return -EFAULT; + + ret = tdx_mcall_get_report0(reportdata, tdx_report_buf); + if (WARN_ONCE(ret, "tdx_mcall_get_report0() failed: %d", ret)) + return ret; + + if (!sockptr_is_null(tdreport) && + copy_to_sockptr(tdreport, tdx_report_buf, TDX_REPORT_LEN)) + return -EFAULT; + } + return 0; +} + +static int tdx_do_extend(u8 mr_ind, const u8 *data) +{ + scoped_cond_guard(mutex_intr, return -EINTR, &mr_lock) { + /* + * TDX requires @extend_buf to be 64-byte aligned. + * It's safe to use REPORTDATA buffer for that purpose because + * tdx_mr_report/extend_lock() are mutually exclusive. + */ + u8 *extend_buf = tdx_report_buf + TDREPORT_reportdata; + int ret; + + memcpy(extend_buf, data, SHA384_DIGEST_SIZE); + + ret = tdx_mcall_extend_rtmr(mr_ind, extend_buf); + if (WARN_ONCE(ret, "tdx_mcall_extend_rtmr(%u) failed: %d", mr_ind, ret)) + return ret; + } + return 0; +} + +#define TDX_MR_(r) .mr_value = (void *)TDREPORT_##r, TSM_MR_(r, SHA384) +static struct tsm_measurement_register tdx_mrs[] = { + { TDX_MR_(rtmr0) | TSM_MR_F_RTMR }, + { TDX_MR_(rtmr1) | TSM_MR_F_RTMR }, + { TDX_MR_(rtmr2) | TSM_MR_F_RTMR }, + { TDX_MR_(rtmr3) | TSM_MR_F_RTMR }, + { TDX_MR_(mrtd) }, + { TDX_MR_(mrconfigid) | TSM_MR_F_NOHASH }, + { TDX_MR_(mrowner) | TSM_MR_F_NOHASH }, + { TDX_MR_(mrownerconfig) | TSM_MR_F_NOHASH }, +}; +#undef TDX_MR_ + +static int tdx_mr_refresh(const struct tsm_measurements *tm) +{ + return tdx_do_report(KERNEL_SOCKPTR(NULL), KERNEL_SOCKPTR(NULL)); +} + +static int tdx_mr_extend(const struct tsm_measurements *tm, + const struct tsm_measurement_register *mr, + const u8 *data) +{ + return tdx_do_extend(mr - tm->mrs, data); +} + +static struct tsm_measurements tdx_measurements = { + .mrs = tdx_mrs, + .nr_mrs = ARRAY_SIZE(tdx_mrs), + .refresh = tdx_mr_refresh, + .write = tdx_mr_extend, +}; + +static const struct attribute_group *tdx_mr_init(void) +{ + const struct attribute_group *g; + int rc; + + u8 *buf __free(kfree) = kzalloc(TDX_REPORT_LEN, GFP_KERNEL); + if (!buf) + return ERR_PTR(-ENOMEM); + + tdx_report_buf = buf; + rc = tdx_mr_refresh(&tdx_measurements); + if (rc) + return ERR_PTR(rc); + + /* + * @mr_value was initialized with the offset only, while the base + * address is being added here. + */ + for (size_t i = 0; i < ARRAY_SIZE(tdx_mrs); ++i) + *(long *)&tdx_mrs[i].mr_value += (long)buf; + + g = tsm_mr_create_attribute_group(&tdx_measurements); + if (!IS_ERR(g)) + tdx_report_buf = no_free_ptr(buf); + + return g; +} + +static void tdx_mr_deinit(const struct attribute_group *mr_grp) +{ + tsm_mr_free_attribute_group(mr_grp); + kfree(tdx_report_buf); +} + +/* + * Intel's SGX QE implementation generally uses Quote size less + * than 8K (2K Quote data + ~5K of certificate blob). + * DICE-based attestation uses layered evidence that requires + * larger Quote size (~100K). + */ +#define GET_QUOTE_BUF_SIZE SZ_128K + +#define GET_QUOTE_CMD_VER 1 + +/* TDX GetQuote status codes */ +#define GET_QUOTE_SUCCESS 0 +#define GET_QUOTE_IN_FLIGHT 0xffffffffffffffff + +#define TDX_QUOTE_MAX_LEN (GET_QUOTE_BUF_SIZE - sizeof(struct tdx_quote_buf)) + +/* struct tdx_quote_buf: Format of Quote request buffer. + * @version: Quote format version, filled by TD. + * @status: Status code of Quote request, filled by VMM. + * @in_len: Length of TDREPORT, filled by TD. + * @out_len: Length of Quote data, filled by VMM. + * @data: Quote data on output or TDREPORT on input. + * + * More details of Quote request buffer can be found in TDX + * Guest-Host Communication Interface (GHCI) for Intel TDX 1.0, + * section titled "TDG.VP.VMCALL<GetQuote>" + */ +struct tdx_quote_buf { + u64 version; + u64 status; + u32 in_len; + u32 out_len; + u8 data[]; +}; + +/* Quote data buffer */ +static void *quote_data; + +/* Lock to streamline quote requests */ +static DEFINE_MUTEX(quote_lock); + +/* + * GetQuote request timeout in seconds. Expect that 30 seconds + * is enough time for QE to respond to any Quote requests. + */ +static u32 getquote_timeout = 30; + +static long tdx_get_report0(struct tdx_report_req __user *req) +{ + return tdx_do_report(USER_SOCKPTR(req->reportdata), + USER_SOCKPTR(req->tdreport)); +} + +static void free_quote_buf(void *buf) +{ + size_t len = PAGE_ALIGN(GET_QUOTE_BUF_SIZE); + unsigned int count = len >> PAGE_SHIFT; + + if (set_memory_encrypted((unsigned long)buf, count)) { + pr_err("Failed to restore encryption mask for Quote buffer, leak it\n"); + return; + } + + free_pages_exact(buf, len); +} + +static void *alloc_quote_buf(void) +{ + size_t len = PAGE_ALIGN(GET_QUOTE_BUF_SIZE); + unsigned int count = len >> PAGE_SHIFT; + void *addr; + + addr = alloc_pages_exact(len, GFP_KERNEL | __GFP_ZERO); + if (!addr) + return NULL; + + if (set_memory_decrypted((unsigned long)addr, count)) + return NULL; + + return addr; +} + +/* + * wait_for_quote_completion() - Wait for Quote request completion + * @quote_buf: Address of Quote buffer. + * @timeout: Timeout in seconds to wait for the Quote generation. + * + * As per TDX GHCI v1.0 specification, sec titled "TDG.VP.VMCALL<GetQuote>", + * the status field in the Quote buffer will be set to GET_QUOTE_IN_FLIGHT + * while VMM processes the GetQuote request, and will change it to success + * or error code after processing is complete. So wait till the status + * changes from GET_QUOTE_IN_FLIGHT or the request being timed out. + */ +static int wait_for_quote_completion(struct tdx_quote_buf *quote_buf, u32 timeout) +{ + int i = 0; + + /* + * Quote requests usually take a few seconds to complete, so waking up + * once per second to recheck the status is fine for this use case. + */ + while (quote_buf->status == GET_QUOTE_IN_FLIGHT && i++ < timeout) { + if (msleep_interruptible(MSEC_PER_SEC)) + return -EINTR; + } + + return (i == timeout) ? -ETIMEDOUT : 0; +} + +static int tdx_report_new_locked(struct tsm_report *report, void *data) +{ + u8 *buf; + struct tdx_quote_buf *quote_buf = quote_data; + struct tsm_report_desc *desc = &report->desc; + u32 out_len; + int ret; + u64 err; + + /* + * If the previous request is timedout or interrupted, and the + * Quote buf status is still in GET_QUOTE_IN_FLIGHT (owned by + * VMM), don't permit any new request. + */ + if (quote_buf->status == GET_QUOTE_IN_FLIGHT) + return -EBUSY; + + if (desc->inblob_len != TDX_REPORTDATA_LEN) + return -EINVAL; + + memset(quote_data, 0, GET_QUOTE_BUF_SIZE); + + /* Update Quote buffer header */ + quote_buf->version = GET_QUOTE_CMD_VER; + quote_buf->in_len = TDX_REPORT_LEN; + + ret = tdx_do_report(KERNEL_SOCKPTR(desc->inblob), + KERNEL_SOCKPTR(quote_buf->data)); + if (ret) + return ret; + + err = tdx_hcall_get_quote(quote_data, GET_QUOTE_BUF_SIZE); + if (err) { + pr_err("GetQuote hypercall failed, status:%llx\n", err); + return -EIO; + } + + ret = wait_for_quote_completion(quote_buf, getquote_timeout); + if (ret) { + pr_err("GetQuote request timedout\n"); + return ret; + } + + if (quote_buf->status != GET_QUOTE_SUCCESS) { + pr_debug("GetQuote request failed, status:%llx\n", quote_buf->status); + return -EIO; + } + + out_len = READ_ONCE(quote_buf->out_len); + + if (out_len > TDX_QUOTE_MAX_LEN) + return -EFBIG; + + buf = kvmemdup(quote_buf->data, out_len, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + report->outblob = buf; + report->outblob_len = out_len; + + /* + * TODO: parse the PEM-formatted cert chain out of the quote buffer when + * provided + */ + + return ret; +} + +static int tdx_report_new(struct tsm_report *report, void *data) +{ + scoped_cond_guard(mutex_intr, return -EINTR, "e_lock) + return tdx_report_new_locked(report, data); +} + +static bool tdx_report_attr_visible(int n) +{ + switch (n) { + case TSM_REPORT_GENERATION: + case TSM_REPORT_PROVIDER: + return true; + } + + return false; +} + +static bool tdx_report_bin_attr_visible(int n) +{ + switch (n) { + case TSM_REPORT_INBLOB: + case TSM_REPORT_OUTBLOB: + return true; + } + + return false; +} + +static long tdx_guest_ioctl(struct file *file, unsigned int cmd, + unsigned long arg) +{ + switch (cmd) { + case TDX_CMD_GET_REPORT0: + return tdx_get_report0((struct tdx_report_req __user *)arg); + default: + return -ENOTTY; + } +} + +static const struct file_operations tdx_guest_fops = { + .owner = THIS_MODULE, + .unlocked_ioctl = tdx_guest_ioctl, +}; + +static const struct attribute_group *tdx_attr_groups[] = { + NULL, /* measurements */ + NULL +}; + +static struct miscdevice tdx_misc_dev = { + .name = KBUILD_MODNAME, + .minor = MISC_DYNAMIC_MINOR, + .fops = &tdx_guest_fops, + .groups = tdx_attr_groups, +}; + +static const struct x86_cpu_id tdx_guest_ids[] = { + X86_MATCH_FEATURE(X86_FEATURE_TDX_GUEST, NULL), + {} +}; +MODULE_DEVICE_TABLE(x86cpu, tdx_guest_ids); + +static const struct tsm_report_ops tdx_tsm_ops = { + .name = KBUILD_MODNAME, + .report_new = tdx_report_new, + .report_attr_visible = tdx_report_attr_visible, + .report_bin_attr_visible = tdx_report_bin_attr_visible, +}; + +static int __init tdx_guest_init(void) +{ + int ret; + + if (!x86_match_cpu(tdx_guest_ids)) + return -ENODEV; + + tdx_attr_groups[0] = tdx_mr_init(); + if (IS_ERR(tdx_attr_groups[0])) + return PTR_ERR(tdx_attr_groups[0]); + + ret = misc_register(&tdx_misc_dev); + if (ret) + goto deinit_mr; + + quote_data = alloc_quote_buf(); + if (!quote_data) { + pr_err("Failed to allocate Quote buffer\n"); + ret = -ENOMEM; + goto free_misc; + } + + ret = tsm_report_register(&tdx_tsm_ops, NULL); + if (ret) + goto free_quote; + + return 0; + +free_quote: + free_quote_buf(quote_data); +free_misc: + misc_deregister(&tdx_misc_dev); +deinit_mr: + tdx_mr_deinit(tdx_attr_groups[0]); + + return ret; +} +module_init(tdx_guest_init); + +static void __exit tdx_guest_exit(void) +{ + tsm_report_unregister(&tdx_tsm_ops); + free_quote_buf(quote_data); + misc_deregister(&tdx_misc_dev); + tdx_mr_deinit(tdx_attr_groups[0]); +} +module_exit(tdx_guest_exit); + +MODULE_AUTHOR("Kuppuswamy Sathyanarayanan <sathyanarayanan.kuppuswamy@linux.intel.com>"); +MODULE_DESCRIPTION("TDX Guest Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/virt/coco/tdx-host/Kconfig b/drivers/virt/coco/tdx-host/Kconfig new file mode 100644 index 000000000..57d0c01a4 --- /dev/null +++ b/drivers/virt/coco/tdx-host/Kconfig @@ -0,0 +1,6 @@ +config TDX_HOST_SERVICES + tristate + depends on INTEL_TDX_HOST + select FW_LOADER + select FW_UPLOAD + default m diff --git a/drivers/virt/coco/tdx-host/Makefile b/drivers/virt/coco/tdx-host/Makefile new file mode 100644 index 000000000..e61e749a8 --- /dev/null +++ b/drivers/virt/coco/tdx-host/Makefile @@ -0,0 +1 @@ +obj-$(CONFIG_TDX_HOST_SERVICES) += tdx-host.o diff --git a/drivers/virt/coco/tdx-host/tdx-host.c b/drivers/virt/coco/tdx-host/tdx-host.c new file mode 100644 index 000000000..e8ed8dfa5 --- /dev/null +++ b/drivers/virt/coco/tdx-host/tdx-host.c @@ -0,0 +1,238 @@ +// 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"); diff --git a/drivers/virt/coco/tsm-core.c b/drivers/virt/coco/tsm-core.c new file mode 100644 index 000000000..e78499335 --- /dev/null +++ b/drivers/virt/coco/tsm-core.c @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright(c) 2024-2025 Intel Corporation. All rights reserved. */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/tsm.h> +#include <linux/device.h> +#include <linux/module.h> +#include <linux/cleanup.h> +#include <linux/pci-tsm.h> + +static void tsm_release(struct device *); +static const struct class tsm_class = { + .name = "tsm", + .dev_release = tsm_release +}; +static DEFINE_IDA(tsm_ida); + +static int match_id(struct device *dev, const void *data) +{ + struct tsm_dev *tsm_dev = container_of(dev, struct tsm_dev, dev); + int id = *(const int *)data; + + return tsm_dev->id == id; +} + +struct tsm_dev *find_tsm_dev(int id) +{ + struct device *dev = class_find_device(&tsm_class, NULL, &id, match_id); + + if (!dev) + return NULL; + return container_of(dev, struct tsm_dev, dev); +} + +static struct tsm_dev *alloc_tsm_dev(struct device *parent) +{ + struct device *dev; + int id; + + struct tsm_dev *tsm_dev __free(kfree) = + kzalloc_obj(*tsm_dev); + if (!tsm_dev) + return ERR_PTR(-ENOMEM); + + id = ida_alloc(&tsm_ida, GFP_KERNEL); + if (id < 0) + return ERR_PTR(id); + + tsm_dev->id = id; + dev = &tsm_dev->dev; + dev->parent = parent; + dev->class = &tsm_class; + device_initialize(dev); + + return no_free_ptr(tsm_dev); +} + +static struct tsm_dev *tsm_register_pci_or_reset(struct tsm_dev *tsm_dev, + struct pci_tsm_ops *pci_ops) +{ + int rc; + + if (!pci_ops) + return tsm_dev; + + tsm_dev->pci_ops = pci_ops; + rc = pci_tsm_register(tsm_dev); + if (rc) { + dev_err(tsm_dev->dev.parent, + "PCI/TSM registration failure: %d\n", rc); + device_unregister(&tsm_dev->dev); + return ERR_PTR(rc); + } + + /* Notify TSM userspace that PCI/TSM operations are now possible */ + kobject_uevent(&tsm_dev->dev.kobj, KOBJ_CHANGE); + return tsm_dev; +} + +struct tsm_dev *tsm_register(struct device *parent, struct pci_tsm_ops *pci_ops) +{ + struct tsm_dev *tsm_dev __free(put_tsm_dev) = alloc_tsm_dev(parent); + struct device *dev; + int rc; + + if (IS_ERR(tsm_dev)) + return tsm_dev; + + dev = &tsm_dev->dev; + rc = dev_set_name(dev, "tsm%d", tsm_dev->id); + if (rc) + return ERR_PTR(rc); + + rc = device_add(dev); + if (rc) + return ERR_PTR(rc); + + return tsm_register_pci_or_reset(no_free_ptr(tsm_dev), pci_ops); +} +EXPORT_SYMBOL_GPL(tsm_register); + +void tsm_unregister(struct tsm_dev *tsm_dev) +{ + if (tsm_dev->pci_ops) + pci_tsm_unregister(tsm_dev); + device_unregister(&tsm_dev->dev); +} +EXPORT_SYMBOL_GPL(tsm_unregister); + +static void tsm_release(struct device *dev) +{ + struct tsm_dev *tsm_dev = container_of(dev, typeof(*tsm_dev), dev); + + ida_free(&tsm_ida, tsm_dev->id); + kfree(tsm_dev); +} + +static int __init tsm_init(void) +{ + return class_register(&tsm_class); +} +module_init(tsm_init) + +static void __exit tsm_exit(void) +{ + class_unregister(&tsm_class); +} +module_exit(tsm_exit) + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("TEE Security Manager Class Device"); |
