From dc50eab76b709d68175a358d6e23a5a3890764d3 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 18 May 2024 19:39:57 +0200 Subject: Merging upstream version 6.7.7. Signed-off-by: Daniel Baumann --- drivers/virt/Kconfig | 6 +- drivers/virt/Makefile | 4 +- drivers/virt/acrn/acrn_drv.h | 2 +- drivers/virt/acrn/hsm.c | 2 +- drivers/virt/acrn/mm.c | 2 +- drivers/virt/coco/Kconfig | 14 ++ drivers/virt/coco/Makefile | 8 + drivers/virt/coco/sev-guest/Kconfig | 1 + drivers/virt/coco/sev-guest/sev-guest.c | 179 +++++++++++++- drivers/virt/coco/tdx-guest/Kconfig | 1 + drivers/virt/coco/tdx-guest/tdx-guest.c | 229 ++++++++++++++++- drivers/virt/coco/tsm.c | 425 ++++++++++++++++++++++++++++++++ 12 files changed, 849 insertions(+), 24 deletions(-) create mode 100644 drivers/virt/coco/Kconfig create mode 100644 drivers/virt/coco/Makefile create mode 100644 drivers/virt/coco/tsm.c (limited to 'drivers/virt') diff --git a/drivers/virt/Kconfig b/drivers/virt/Kconfig index f79ab13a5c..40129b6f0e 100644 --- a/drivers/virt/Kconfig +++ b/drivers/virt/Kconfig @@ -48,10 +48,6 @@ source "drivers/virt/nitro_enclaves/Kconfig" source "drivers/virt/acrn/Kconfig" -source "drivers/virt/coco/efi_secret/Kconfig" - -source "drivers/virt/coco/sev-guest/Kconfig" - -source "drivers/virt/coco/tdx-guest/Kconfig" +source "drivers/virt/coco/Kconfig" endif diff --git a/drivers/virt/Makefile b/drivers/virt/Makefile index e9aa6fc96f..f29901bd78 100644 --- a/drivers/virt/Makefile +++ b/drivers/virt/Makefile @@ -9,6 +9,4 @@ obj-y += vboxguest/ obj-$(CONFIG_NITRO_ENCLAVES) += nitro_enclaves/ obj-$(CONFIG_ACRN_HSM) += acrn/ -obj-$(CONFIG_EFI_SECRET) += coco/efi_secret/ -obj-$(CONFIG_SEV_GUEST) += coco/sev-guest/ -obj-$(CONFIG_INTEL_TDX_GUEST) += coco/tdx-guest/ +obj-y += coco/ diff --git a/drivers/virt/acrn/acrn_drv.h b/drivers/virt/acrn/acrn_drv.h index 5663c17ad3..fb8438094f 100644 --- a/drivers/virt/acrn/acrn_drv.h +++ b/drivers/virt/acrn/acrn_drv.h @@ -60,7 +60,7 @@ struct vm_memory_region_batch { u16 reserved[3]; u32 regions_num; u64 regions_gpa; - struct vm_memory_region_op regions_op[]; + struct vm_memory_region_op regions_op[] __counted_by(regions_num); }; /** diff --git a/drivers/virt/acrn/hsm.c b/drivers/virt/acrn/hsm.c index 423ea888d7..c24036c4e5 100644 --- a/drivers/virt/acrn/hsm.c +++ b/drivers/virt/acrn/hsm.c @@ -447,7 +447,7 @@ static ssize_t remove_cpu_store(struct device *dev, if (cpu_online(cpu)) remove_cpu(cpu); - lapicid = cpu_data(cpu).apicid; + lapicid = cpu_data(cpu).topo.apicid; dev_dbg(dev, "Try to remove cpu %lld with lapicid %lld\n", cpu, lapicid); ret = hcall_sos_remove_cpu(lapicid); if (ret < 0) { diff --git a/drivers/virt/acrn/mm.c b/drivers/virt/acrn/mm.c index b4ad8d452e..fa5d9ca6be 100644 --- a/drivers/virt/acrn/mm.c +++ b/drivers/virt/acrn/mm.c @@ -250,11 +250,11 @@ int acrn_vm_ram_map(struct acrn_vm *vm, struct acrn_vm_memmap *memmap) ret = -ENOMEM; goto unmap_kernel_map; } + regions_info->regions_num = nr_regions; /* Fill each vm_memory_region_op */ vm_region = regions_info->regions_op; regions_info->vmid = vm->vmid; - regions_info->regions_num = nr_regions; regions_info->regions_gpa = virt_to_phys(vm_region); user_vm_pa = memmap->user_vm_pa; i = 0; diff --git a/drivers/virt/coco/Kconfig b/drivers/virt/coco/Kconfig new file mode 100644 index 0000000000..87d142c1f9 --- /dev/null +++ b/drivers/virt/coco/Kconfig @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Confidential computing related collateral +# + +config TSM_REPORTS + select CONFIGFS_FS + tristate + +source "drivers/virt/coco/efi_secret/Kconfig" + +source "drivers/virt/coco/sev-guest/Kconfig" + +source "drivers/virt/coco/tdx-guest/Kconfig" diff --git a/drivers/virt/coco/Makefile b/drivers/virt/coco/Makefile new file mode 100644 index 0000000000..18c1aba5ed --- /dev/null +++ b/drivers/virt/coco/Makefile @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Confidential computing related collateral +# +obj-$(CONFIG_TSM_REPORTS) += tsm.o +obj-$(CONFIG_EFI_SECRET) += efi_secret/ +obj-$(CONFIG_SEV_GUEST) += sev-guest/ +obj-$(CONFIG_INTEL_TDX_GUEST) += tdx-guest/ diff --git a/drivers/virt/coco/sev-guest/Kconfig b/drivers/virt/coco/sev-guest/Kconfig index da2d7ca531..1cffc72c41 100644 --- a/drivers/virt/coco/sev-guest/Kconfig +++ b/drivers/virt/coco/sev-guest/Kconfig @@ -5,6 +5,7 @@ config SEV_GUEST select CRYPTO select CRYPTO_AEAD2 select CRYPTO_GCM + 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, diff --git a/drivers/virt/coco/sev-guest/sev-guest.c b/drivers/virt/coco/sev-guest/sev-guest.c index 5bee58ef5f..bc564adcf4 100644 --- a/drivers/virt/coco/sev-guest/sev-guest.c +++ b/drivers/virt/coco/sev-guest/sev-guest.c @@ -16,9 +16,13 @@ #include #include #include +#include #include #include #include +#include +#include +#include #include #include @@ -475,6 +479,11 @@ static int handle_guest_request(struct snp_guest_dev *snp_dev, u64 exit_code, return 0; } +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_guest_crypto *crypto = snp_dev->crypto; @@ -555,22 +564,25 @@ static int get_derived_key(struct snp_guest_dev *snp_dev, struct snp_guest_reque return rc; } -static int get_ext_report(struct snp_guest_dev *snp_dev, struct snp_guest_request_ioctl *arg) +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 *req = &snp_dev->req.ext_report; struct snp_guest_crypto *crypto = snp_dev->crypto; struct snp_report_resp *resp; int ret, npages = 0, resp_len; + sockptr_t certs_address; lockdep_assert_held(&snp_cmd_mutex); - if (!arg->req_data || !arg->resp_data) + if (sockptr_is_null(io->req_data) || sockptr_is_null(io->resp_data)) return -EINVAL; - if (copy_from_user(req, (void __user *)arg->req_data, sizeof(*req))) + if (copy_from_sockptr(req, io->req_data, sizeof(*req))) return -EFAULT; - /* userspace does not want certificate data */ + /* caller does not want certificate data */ if (!req->certs_len || !req->certs_address) goto cmd; @@ -578,8 +590,13 @@ static int get_ext_report(struct snp_guest_dev *snp_dev, struct snp_guest_reques !IS_ALIGNED(req->certs_len, PAGE_SIZE)) return -EINVAL; - if (!access_ok((const void __user *)req->certs_address, req->certs_len)) - return -EFAULT; + if (sockptr_is_kernel(io->resp_data)) { + certs_address = KERNEL_SOCKPTR((void *)req->certs_address); + } else { + certs_address = USER_SOCKPTR((void __user *)req->certs_address); + if (!access_ok(certs_address.user, req->certs_len)) + return -EFAULT; + } /* * Initialize the intermediate buffer with all zeros. This buffer @@ -609,21 +626,19 @@ cmd: if (arg->vmm_error == SNP_GUEST_VMM_ERR_INVALID_LEN) { req->certs_len = snp_dev->input.data_npages << PAGE_SHIFT; - if (copy_to_user((void __user *)arg->req_data, req, sizeof(*req))) + if (copy_to_sockptr(io->req_data, req, sizeof(*req))) ret = -EFAULT; } if (ret) goto e_free; - if (npages && - copy_to_user((void __user *)req->certs_address, snp_dev->certs_data, - req->certs_len)) { + if (npages && copy_to_sockptr(certs_address, snp_dev->certs_data, req->certs_len)) { ret = -EFAULT; goto e_free; } - if (copy_to_user((void __user *)arg->resp_data, resp, sizeof(*resp))) + if (copy_to_sockptr(io->resp_data, resp, sizeof(*resp))) ret = -EFAULT; e_free: @@ -636,6 +651,7 @@ static long snp_guest_ioctl(struct file *file, unsigned int ioctl, unsigned long 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))) @@ -664,7 +680,14 @@ static long snp_guest_ioctl(struct file *file, unsigned int ioctl, unsigned long ret = get_derived_key(snp_dev, &input); break; case SNP_GET_EXT_REPORT: - ret = get_ext_report(snp_dev, &input); + /* + * 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; @@ -748,6 +771,130 @@ static u8 *get_vmpck(int id, struct snp_secrets_page_layout *layout, u32 **seqno return key; } +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_report_new(struct tsm_report *report, void *data) +{ + struct snp_msg_cert_entry *cert_table; + struct tsm_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; + + void *buf __free(kvfree) = kvzalloc(size, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + guard(mutex)(&snp_cmd_mutex); + + /* Check if the VMPCK is not empty */ + if (is_vmpck_empty(snp_dev)) { + dev_err_ratelimited(snp_dev->dev, "VMPCK is disabled\n"); + return -ENOTTY; + } + + 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 const struct tsm_ops sev_tsm_ops = { + .name = KBUILD_MODNAME, + .report_new = sev_report_new, +}; + +static void unregister_sev_tsm(void *data) +{ + tsm_unregister(&sev_tsm_ops); +} + static int __init sev_guest_probe(struct platform_device *pdev) { struct snp_secrets_page_layout *layout; @@ -821,6 +968,14 @@ static int __init sev_guest_probe(struct platform_device *pdev) snp_dev->input.resp_gpa = __pa(snp_dev->response); snp_dev->input.data_gpa = __pa(snp_dev->certs_data); + ret = tsm_register(&sev_tsm_ops, snp_dev, &tsm_report_extra_type); + if (ret) + goto e_free_cert_data; + + ret = devm_add_action_or_reset(&pdev->dev, unregister_sev_tsm, NULL); + if (ret) + goto e_free_cert_data; + ret = misc_register(misc); if (ret) goto e_free_cert_data; diff --git a/drivers/virt/coco/tdx-guest/Kconfig b/drivers/virt/coco/tdx-guest/Kconfig index 14246fc2fb..22dd59e194 100644 --- a/drivers/virt/coco/tdx-guest/Kconfig +++ b/drivers/virt/coco/tdx-guest/Kconfig @@ -1,6 +1,7 @@ config TDX_GUEST_DRIVER tristate "TDX Guest driver" depends on INTEL_TDX_GUEST + select TSM_REPORTS help The driver provides userspace interface to communicate with the TDX module to request the TDX guest details like attestation diff --git a/drivers/virt/coco/tdx-guest/tdx-guest.c b/drivers/virt/coco/tdx-guest/tdx-guest.c index 5e44a0fa69..1253bf76b5 100644 --- a/drivers/virt/coco/tdx-guest/tdx-guest.c +++ b/drivers/virt/coco/tdx-guest/tdx-guest.c @@ -12,12 +12,60 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include +/* + * Intel's SGX QE implementation generally uses Quote size less + * than 8K (2K Quote data + ~5K of certificate blob). + */ +#define GET_QUOTE_BUF_SIZE SZ_8K + +#define GET_QUOTE_CMD_VER 1 + +/* TDX GetQuote status codes */ +#define GET_QUOTE_SUCCESS 0 +#define GET_QUOTE_IN_FLIGHT 0xffffffffffffffff + +/* 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" + */ +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) { u8 *reportdata, *tdreport; @@ -53,6 +101,154 @@ out: return ret; } +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)) { + free_pages_exact(addr, len); + 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", + * 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(struct tsm_report *report, void *data) +{ + u8 *buf, *reportdata = NULL, *tdreport = NULL; + struct tdx_quote_buf *quote_buf = quote_data; + struct tsm_desc *desc = &report->desc; + int ret; + u64 err; + + /* TODO: switch to guard(mutex_intr) */ + if (mutex_lock_interruptible("e_lock)) + return -EINTR; + + /* + * 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) { + ret = -EBUSY; + goto done; + } + + if (desc->inblob_len != TDX_REPORTDATA_LEN) { + ret = -EINVAL; + goto done; + } + + reportdata = kmalloc(TDX_REPORTDATA_LEN, GFP_KERNEL); + if (!reportdata) { + ret = -ENOMEM; + goto done; + } + + tdreport = kzalloc(TDX_REPORT_LEN, GFP_KERNEL); + if (!tdreport) { + ret = -ENOMEM; + goto done; + } + + memcpy(reportdata, desc->inblob, desc->inblob_len); + + /* Generate TDREPORT0 using "TDG.MR.REPORT" TDCALL */ + ret = tdx_mcall_get_report0(reportdata, tdreport); + if (ret) { + pr_err("GetReport call failed\n"); + goto done; + } + + 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; + + memcpy(quote_buf->data, tdreport, TDX_REPORT_LEN); + + err = tdx_hcall_get_quote(quote_data, GET_QUOTE_BUF_SIZE); + if (err) { + pr_err("GetQuote hypercall failed, status:%llx\n", err); + ret = -EIO; + goto done; + } + + ret = wait_for_quote_completion(quote_buf, getquote_timeout); + if (ret) { + pr_err("GetQuote request timedout\n"); + goto done; + } + + buf = kvmemdup(quote_buf->data, quote_buf->out_len, GFP_KERNEL); + if (!buf) { + ret = -ENOMEM; + goto done; + } + + report->outblob = buf; + report->outblob_len = quote_buf->out_len; + + /* + * TODO: parse the PEM-formatted cert chain out of the quote buffer when + * provided + */ +done: + mutex_unlock("e_lock); + kfree(reportdata); + kfree(tdreport); + + return ret; +} + static long tdx_guest_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { @@ -82,17 +278,48 @@ static const struct x86_cpu_id tdx_guest_ids[] = { }; MODULE_DEVICE_TABLE(x86cpu, tdx_guest_ids); +static const struct tsm_ops tdx_tsm_ops = { + .name = KBUILD_MODNAME, + .report_new = tdx_report_new, +}; + static int __init tdx_guest_init(void) { + int ret; + if (!x86_match_cpu(tdx_guest_ids)) return -ENODEV; - return misc_register(&tdx_misc_dev); + ret = misc_register(&tdx_misc_dev); + if (ret) + return ret; + + quote_data = alloc_quote_buf(); + if (!quote_data) { + pr_err("Failed to allocate Quote buffer\n"); + ret = -ENOMEM; + goto free_misc; + } + + ret = tsm_register(&tdx_tsm_ops, NULL, NULL); + if (ret) + goto free_quote; + + return 0; + +free_quote: + free_quote_buf(quote_data); +free_misc: + misc_deregister(&tdx_misc_dev); + + return ret; } module_init(tdx_guest_init); static void __exit tdx_guest_exit(void) { + tsm_unregister(&tdx_tsm_ops); + free_quote_buf(quote_data); misc_deregister(&tdx_misc_dev); } module_exit(tdx_guest_exit); diff --git a/drivers/virt/coco/tsm.c b/drivers/virt/coco/tsm.c new file mode 100644 index 0000000000..d1c2db83a8 --- /dev/null +++ b/drivers/virt/coco/tsm.c @@ -0,0 +1,425 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright(c) 2023 Intel Corporation. All rights reserved. */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include + +static struct tsm_provider { + const struct tsm_ops *ops; + const struct config_item_type *type; + void *data; +} 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'. + */ + +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, +}; + +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; + + /* + * 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_PRIVLEVEL_MAX) + return -EINVAL; + + guard(rwsem_write)(&tsm_rwsem); + 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); + return sysfs_emit(buf, "%u\n", provider.ops->privlevel_floor); +} +CONFIGFS_ATTR_RO(tsm_report_, privlevel_floor); + +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_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); + 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 { + 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 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_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 -ENOTTY; + 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); + report->outblob = NULL; + report->auxblob = 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_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_OUTBLOB_MAX); + +#define TSM_DEFAULT_ATTRS() \ + &tsm_report_attr_generation, \ + &tsm_report_attr_provider + +static struct configfs_attribute *tsm_report_attrs[] = { + TSM_DEFAULT_ATTRS(), + NULL, +}; + +static struct configfs_attribute *tsm_report_extra_attrs[] = { + TSM_DEFAULT_ATTRS(), + &tsm_report_attr_privlevel, + &tsm_report_attr_privlevel_floor, + NULL, +}; + +#define TSM_DEFAULT_BIN_ATTRS() \ + &tsm_report_attr_inblob, \ + &tsm_report_attr_outblob + +static struct configfs_bin_attribute *tsm_report_bin_attrs[] = { + TSM_DEFAULT_BIN_ATTRS(), + NULL, +}; + +static struct configfs_bin_attribute *tsm_report_bin_extra_attrs[] = { + TSM_DEFAULT_BIN_ATTRS(), + &tsm_report_attr_auxblob, + 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->auxblob); + kvfree(report->outblob); + kfree(state); +} + +static struct configfs_item_operations tsm_report_item_ops = { + .release = tsm_report_item_release, +}; + +const struct config_item_type tsm_report_default_type = { + .ct_owner = THIS_MODULE, + .ct_bin_attrs = tsm_report_bin_attrs, + .ct_attrs = tsm_report_attrs, + .ct_item_ops = &tsm_report_item_ops, +}; +EXPORT_SYMBOL_GPL(tsm_report_default_type); + +const struct config_item_type tsm_report_extra_type = { + .ct_owner = THIS_MODULE, + .ct_bin_attrs = tsm_report_bin_extra_attrs, + .ct_attrs = tsm_report_extra_attrs, + .ct_item_ops = &tsm_report_item_ops, +}; +EXPORT_SYMBOL_GPL(tsm_report_extra_type); + +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(sizeof(*state), GFP_KERNEL); + if (!state) + return ERR_PTR(-ENOMEM); + + config_item_init_type_name(&state->cfg, name, provider.type); + return &state->cfg; +} + +static struct configfs_group_operations tsm_report_group_ops = { + .make_item = tsm_report_make_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_register(const struct tsm_ops *ops, void *priv, + const struct config_item_type *type) +{ + const struct tsm_ops *conflict; + + if (!type) + type = &tsm_report_default_type; + if (!(type == &tsm_report_default_type || type == &tsm_report_extra_type)) + return -EINVAL; + + guard(rwsem_write)(&tsm_rwsem); + conflict = provider.ops; + if (conflict) { + pr_err("\"%s\" ops already registered\n", conflict->name); + return -EBUSY; + } + + provider.ops = ops; + provider.data = priv; + provider.type = type; + return 0; +} +EXPORT_SYMBOL_GPL(tsm_register); + +int tsm_unregister(const struct tsm_ops *ops) +{ + guard(rwsem_write)(&tsm_rwsem); + if (ops != provider.ops) + return -EBUSY; + provider.ops = NULL; + provider.data = NULL; + provider.type = NULL; + return 0; +} +EXPORT_SYMBOL_GPL(tsm_unregister); + +static struct config_group *tsm_report_group; + +static int __init tsm_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_init); + +static void __exit tsm_exit(void) +{ + configfs_unregister_default_group(tsm_report_group); + configfs_unregister_subsystem(&tsm_configfs); +} +module_exit(tsm_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Provide Trusted Security Module attestation reports via configfs"); -- cgit v1.2.3