| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * EFI variable service via TEE |
| * |
| * Copyright (C) 2022 Linaro |
| */ |
| |
| #include <linux/efi.h> |
| #include <linux/kernel.h> |
| #include <linux/slab.h> |
| #include <linux/tee.h> |
| #include <linux/tee_drv.h> |
| #include <linux/ucs2_string.h> |
| #include "mm_communication.h" |
| |
| static struct efivars tee_efivars; |
| static struct efivar_operations tee_efivar_ops; |
| |
| static size_t max_buffer_size; /* comm + var + func + data */ |
| static size_t max_payload_size; /* func + data */ |
| |
| struct tee_stmm_efi_private { |
| struct tee_context *ctx; |
| u32 session; |
| struct device *dev; |
| }; |
| |
| static struct tee_stmm_efi_private pvt_data; |
| |
| /* UUID of the stmm PTA */ |
| static const struct tee_client_device_id tee_stmm_efi_id_table[] = { |
| {PTA_STMM_UUID}, |
| {} |
| }; |
| |
| static int tee_ctx_match(struct tee_ioctl_version_data *ver, const void *data) |
| { |
| /* currently only OP-TEE is supported as a communication path */ |
| if (ver->impl_id == TEE_IMPL_ID_OPTEE) |
| return 1; |
| else |
| return 0; |
| } |
| |
| /** |
| * tee_mm_communicate() - Pass a buffer to StandaloneMM running in TEE |
| * |
| * @comm_buf: locally allocated communication buffer |
| * @dsize: buffer size |
| * Return: status code |
| */ |
| static efi_status_t tee_mm_communicate(void *comm_buf, size_t dsize) |
| { |
| size_t buf_size; |
| struct efi_mm_communicate_header *mm_hdr; |
| struct tee_ioctl_invoke_arg arg; |
| struct tee_param param[4]; |
| struct tee_shm *shm = NULL; |
| int rc; |
| |
| if (!comm_buf) |
| return EFI_INVALID_PARAMETER; |
| |
| mm_hdr = (struct efi_mm_communicate_header *)comm_buf; |
| buf_size = mm_hdr->message_len + sizeof(efi_guid_t) + sizeof(size_t); |
| |
| if (dsize != buf_size) |
| return EFI_INVALID_PARAMETER; |
| |
| shm = tee_shm_register_kernel_buf(pvt_data.ctx, comm_buf, buf_size); |
| if (IS_ERR(shm)) { |
| dev_err(pvt_data.dev, "Unable to register shared memory\n"); |
| return EFI_UNSUPPORTED; |
| } |
| |
| memset(&arg, 0, sizeof(arg)); |
| arg.func = PTA_STMM_CMD_COMMUNICATE; |
| arg.session = pvt_data.session; |
| arg.num_params = 4; |
| |
| memset(param, 0, sizeof(param)); |
| param[0].attr = TEE_IOCTL_PARAM_ATTR_TYPE_MEMREF_INOUT; |
| param[0].u.memref.size = buf_size; |
| param[0].u.memref.shm = shm; |
| param[1].attr = TEE_IOCTL_PARAM_ATTR_TYPE_VALUE_OUTPUT; |
| param[2].attr = TEE_IOCTL_PARAM_ATTR_TYPE_NONE; |
| param[3].attr = TEE_IOCTL_PARAM_ATTR_TYPE_NONE; |
| |
| rc = tee_client_invoke_func(pvt_data.ctx, &arg, param); |
| tee_shm_free(shm); |
| |
| if (rc < 0 || arg.ret != 0) { |
| dev_err(pvt_data.dev, |
| "PTA_STMM_CMD_COMMUNICATE invoke error: 0x%x\n", arg.ret); |
| return EFI_DEVICE_ERROR; |
| } |
| |
| switch (param[1].u.value.a) { |
| case ARM_SVC_SPM_RET_SUCCESS: |
| return EFI_SUCCESS; |
| |
| case ARM_SVC_SPM_RET_INVALID_PARAMS: |
| return EFI_INVALID_PARAMETER; |
| |
| case ARM_SVC_SPM_RET_DENIED: |
| return EFI_ACCESS_DENIED; |
| |
| case ARM_SVC_SPM_RET_NO_MEMORY: |
| return EFI_OUT_OF_RESOURCES; |
| |
| default: |
| return EFI_ACCESS_DENIED; |
| } |
| } |
| |
| /** |
| * mm_communicate() - Adjust the communication buffer to StandAlonneMM and send |
| * it to TEE |
| * |
| * @comm_buf: locally allocated communication buffer, buffer should |
| * be enough big to have some headers and payload |
| * @payload_size: payload size |
| * Return: status code |
| */ |
| static efi_status_t mm_communicate(u8 *comm_buf, size_t payload_size) |
| { |
| size_t dsize; |
| efi_status_t ret; |
| struct efi_mm_communicate_header *mm_hdr; |
| struct smm_variable_communicate_header *var_hdr; |
| |
| dsize = payload_size + MM_COMMUNICATE_HEADER_SIZE + |
| MM_VARIABLE_COMMUNICATE_SIZE; |
| mm_hdr = (struct efi_mm_communicate_header *)comm_buf; |
| var_hdr = (struct smm_variable_communicate_header *)mm_hdr->data; |
| |
| ret = tee_mm_communicate(comm_buf, dsize); |
| if (ret != EFI_SUCCESS) { |
| dev_err(pvt_data.dev, "%s failed!\n", __func__); |
| return ret; |
| } |
| |
| return var_hdr->ret_status; |
| } |
| |
| /** |
| * setup_mm_hdr() - Allocate a buffer for StandAloneMM and initialize the |
| * header data. |
| * |
| * @dptr: pointer address to store allocated buffer |
| * @payload_size: payload size |
| * @func: standAloneMM function number |
| * @ret: EFI return code |
| * Return: pointer to corresponding StandAloneMM function buffer or NULL |
| */ |
| static void *setup_mm_hdr(u8 **dptr, size_t payload_size, size_t func, |
| efi_status_t *ret) |
| { |
| const efi_guid_t mm_var_guid = EFI_MM_VARIABLE_GUID; |
| struct efi_mm_communicate_header *mm_hdr; |
| struct smm_variable_communicate_header *var_hdr; |
| u8 *comm_buf; |
| |
| /* In the init function we initialize max_buffer_size with |
| * get_max_payload(). So skip the test if max_buffer_size is initialized |
| * StandAloneMM will perform similar checks and drop the buffer if it's |
| * too long |
| */ |
| if (max_buffer_size && |
| max_buffer_size < (MM_COMMUNICATE_HEADER_SIZE + |
| MM_VARIABLE_COMMUNICATE_SIZE + payload_size)) { |
| *ret = EFI_INVALID_PARAMETER; |
| return NULL; |
| } |
| |
| comm_buf = kzalloc(MM_COMMUNICATE_HEADER_SIZE + |
| MM_VARIABLE_COMMUNICATE_SIZE + payload_size, |
| GFP_KERNEL); |
| if (!comm_buf) { |
| *ret = EFI_OUT_OF_RESOURCES; |
| return NULL; |
| } |
| |
| mm_hdr = (struct efi_mm_communicate_header *)comm_buf; |
| memcpy(&mm_hdr->header_guid, &mm_var_guid, sizeof(mm_hdr->header_guid)); |
| mm_hdr->message_len = MM_VARIABLE_COMMUNICATE_SIZE + payload_size; |
| |
| var_hdr = (struct smm_variable_communicate_header *)mm_hdr->data; |
| var_hdr->function = func; |
| if (dptr) |
| *dptr = comm_buf; |
| *ret = EFI_SUCCESS; |
| |
| return var_hdr->data; |
| } |
| |
| /** |
| * get_max_payload() - Get variable payload size from StandAloneMM. |
| * |
| * @size: size of the variable in storage |
| * Return: status code |
| */ |
| static efi_status_t get_max_payload(size_t *size) |
| { |
| struct smm_variable_payload_size *var_payload = NULL; |
| size_t payload_size; |
| u8 *comm_buf = NULL; |
| efi_status_t ret; |
| |
| if (!size) |
| return EFI_INVALID_PARAMETER; |
| |
| payload_size = sizeof(*var_payload); |
| var_payload = setup_mm_hdr(&comm_buf, payload_size, |
| SMM_VARIABLE_FUNCTION_GET_PAYLOAD_SIZE, |
| &ret); |
| if (!var_payload) |
| return EFI_OUT_OF_RESOURCES; |
| |
| ret = mm_communicate(comm_buf, payload_size); |
| if (ret != EFI_SUCCESS) |
| goto out; |
| |
| /* Make sure the buffer is big enough for storing variables */ |
| if (var_payload->size < MM_VARIABLE_ACCESS_HEADER_SIZE + 0x20) { |
| ret = EFI_DEVICE_ERROR; |
| goto out; |
| } |
| *size = var_payload->size; |
| /* |
| * There seems to be a bug in EDK2 miscalculating the boundaries and |
| * size checks, so deduct 2 more bytes to fulfill this requirement. Fix |
| * it up here to ensure backwards compatibility with older versions |
| * (cf. StandaloneMmPkg/Drivers/StandaloneMmCpu/AArch64/EventHandle.c. |
| * sizeof (EFI_MM_COMMUNICATE_HEADER) instead the size minus the |
| * flexible array member). |
| * |
| * size is guaranteed to be > 2 due to checks on the beginning. |
| */ |
| *size -= 2; |
| out: |
| kfree(comm_buf); |
| return ret; |
| } |
| |
| static efi_status_t get_property_int(u16 *name, size_t name_size, |
| const efi_guid_t *vendor, |
| struct var_check_property *var_property) |
| { |
| struct smm_variable_var_check_property *smm_property; |
| size_t payload_size; |
| u8 *comm_buf = NULL; |
| efi_status_t ret; |
| |
| memset(var_property, 0, sizeof(*var_property)); |
| payload_size = sizeof(*smm_property) + name_size; |
| if (payload_size > max_payload_size) |
| return EFI_INVALID_PARAMETER; |
| |
| smm_property = setup_mm_hdr( |
| &comm_buf, payload_size, |
| SMM_VARIABLE_FUNCTION_VAR_CHECK_VARIABLE_PROPERTY_GET, &ret); |
| if (!smm_property) |
| return EFI_OUT_OF_RESOURCES; |
| |
| memcpy(&smm_property->guid, vendor, sizeof(smm_property->guid)); |
| smm_property->name_size = name_size; |
| memcpy(smm_property->name, name, name_size); |
| |
| ret = mm_communicate(comm_buf, payload_size); |
| /* |
| * Currently only R/O property is supported in StMM. |
| * Variables that are not set to R/O will not set the property in StMM |
| * and the call will return EFI_NOT_FOUND. We are setting the |
| * properties to 0x0 so checking against that is enough for the |
| * EFI_NOT_FOUND case. |
| */ |
| if (ret == EFI_NOT_FOUND) |
| ret = EFI_SUCCESS; |
| if (ret != EFI_SUCCESS) |
| goto out; |
| memcpy(var_property, &smm_property->property, sizeof(*var_property)); |
| |
| out: |
| kfree(comm_buf); |
| return ret; |
| } |
| |
| static efi_status_t tee_get_variable(u16 *name, efi_guid_t *vendor, |
| u32 *attributes, unsigned long *data_size, |
| void *data) |
| { |
| struct var_check_property var_property; |
| struct smm_variable_access *var_acc; |
| size_t payload_size; |
| size_t name_size; |
| size_t tmp_dsize; |
| u8 *comm_buf = NULL; |
| efi_status_t ret; |
| |
| if (!name || !vendor || !data_size) |
| return EFI_INVALID_PARAMETER; |
| |
| name_size = (ucs2_strnlen(name, EFI_VAR_NAME_LEN) + 1) * sizeof(u16); |
| if (name_size > max_payload_size - MM_VARIABLE_ACCESS_HEADER_SIZE) |
| return EFI_INVALID_PARAMETER; |
| |
| /* Trim output buffer size */ |
| tmp_dsize = *data_size; |
| if (name_size + tmp_dsize > |
| max_payload_size - MM_VARIABLE_ACCESS_HEADER_SIZE) { |
| tmp_dsize = max_payload_size - MM_VARIABLE_ACCESS_HEADER_SIZE - |
| name_size; |
| } |
| |
| payload_size = MM_VARIABLE_ACCESS_HEADER_SIZE + name_size + tmp_dsize; |
| var_acc = setup_mm_hdr(&comm_buf, payload_size, |
| SMM_VARIABLE_FUNCTION_GET_VARIABLE, &ret); |
| if (!var_acc) |
| return EFI_OUT_OF_RESOURCES; |
| |
| /* Fill in contents */ |
| memcpy(&var_acc->guid, vendor, sizeof(var_acc->guid)); |
| var_acc->data_size = tmp_dsize; |
| var_acc->name_size = name_size; |
| var_acc->attr = attributes ? *attributes : 0; |
| memcpy(var_acc->name, name, name_size); |
| |
| ret = mm_communicate(comm_buf, payload_size); |
| if (ret == EFI_SUCCESS || ret == EFI_BUFFER_TOO_SMALL) |
| /* Update with reported data size for trimmed case */ |
| *data_size = var_acc->data_size; |
| if (ret != EFI_SUCCESS) |
| goto out; |
| |
| ret = get_property_int(name, name_size, vendor, &var_property); |
| if (ret != EFI_SUCCESS) |
| goto out; |
| |
| if (attributes) |
| *attributes = var_acc->attr; |
| |
| if (!data) { |
| ret = EFI_INVALID_PARAMETER; |
| goto out; |
| } |
| memcpy(data, (u8 *)var_acc->name + var_acc->name_size, |
| var_acc->data_size); |
| out: |
| kfree(comm_buf); |
| return ret; |
| } |
| |
| static efi_status_t tee_get_next_variable(unsigned long *name_size, |
| efi_char16_t *name, efi_guid_t *guid) |
| { |
| struct smm_variable_getnext *var_getnext; |
| size_t payload_size; |
| size_t out_name_size; |
| size_t in_name_size; |
| u8 *comm_buf = NULL; |
| efi_status_t ret; |
| |
| if (!name_size || !name || !guid) |
| return EFI_INVALID_PARAMETER; |
| |
| out_name_size = *name_size; |
| in_name_size = (ucs2_strnlen(name, EFI_VAR_NAME_LEN) + 1) * sizeof(u16); |
| |
| if (out_name_size < in_name_size) |
| return EFI_INVALID_PARAMETER; |
| |
| if (in_name_size > max_payload_size - MM_VARIABLE_GET_NEXT_HEADER_SIZE) |
| return EFI_INVALID_PARAMETER; |
| |
| /* Trim output buffer size */ |
| if (out_name_size > max_payload_size - MM_VARIABLE_GET_NEXT_HEADER_SIZE) |
| out_name_size = |
| max_payload_size - MM_VARIABLE_GET_NEXT_HEADER_SIZE; |
| |
| payload_size = MM_VARIABLE_GET_NEXT_HEADER_SIZE + out_name_size; |
| var_getnext = setup_mm_hdr(&comm_buf, payload_size, |
| SMM_VARIABLE_FUNCTION_GET_NEXT_VARIABLE_NAME, |
| &ret); |
| if (!var_getnext) |
| return EFI_OUT_OF_RESOURCES; |
| |
| /* Fill in contents */ |
| memcpy(&var_getnext->guid, guid, sizeof(var_getnext->guid)); |
| var_getnext->name_size = out_name_size; |
| memcpy(var_getnext->name, name, in_name_size); |
| memset((u8 *)var_getnext->name + in_name_size, 0x0, |
| out_name_size - in_name_size); |
| |
| ret = mm_communicate(comm_buf, payload_size); |
| if (ret == EFI_SUCCESS || ret == EFI_BUFFER_TOO_SMALL) { |
| /* Update with reported data size for trimmed case */ |
| *name_size = var_getnext->name_size; |
| } |
| if (ret != EFI_SUCCESS) |
| goto out; |
| |
| memcpy(guid, &var_getnext->guid, sizeof(*guid)); |
| memcpy(name, var_getnext->name, var_getnext->name_size); |
| |
| out: |
| kfree(comm_buf); |
| return ret; |
| } |
| |
| static efi_status_t tee_set_variable(efi_char16_t *name, efi_guid_t *vendor, |
| u32 attributes, unsigned long data_size, |
| void *data) |
| { |
| efi_status_t ret; |
| struct var_check_property var_property; |
| struct smm_variable_access *var_acc; |
| size_t payload_size; |
| size_t name_size; |
| u8 *comm_buf = NULL; |
| |
| if (!name || name[0] == 0 || !vendor) |
| return EFI_INVALID_PARAMETER; |
| |
| if (data_size > 0 && !data) |
| return EFI_INVALID_PARAMETER; |
| |
| /* Check payload size */ |
| name_size = (ucs2_strnlen(name, EFI_VAR_NAME_LEN) + 1) * sizeof(u16); |
| payload_size = MM_VARIABLE_ACCESS_HEADER_SIZE + name_size + data_size; |
| if (payload_size > max_payload_size) |
| return EFI_INVALID_PARAMETER; |
| |
| /* |
| * Allocate the buffer early, before switching to RW (if needed) |
| * so we won't need to account for any failures in reading/setting |
| * the properties, if the allocation fails |
| */ |
| var_acc = setup_mm_hdr(&comm_buf, payload_size, |
| SMM_VARIABLE_FUNCTION_SET_VARIABLE, &ret); |
| if (!var_acc) |
| return EFI_OUT_OF_RESOURCES; |
| |
| /* |
| * The API has the ability to override RO flags. If no RO check was |
| * requested switch the variable to RW for the duration of this call |
| */ |
| ret = get_property_int(name, name_size, vendor, &var_property); |
| if (ret != EFI_SUCCESS) { |
| dev_err(pvt_data.dev, "Getting variable property failed\n"); |
| goto out; |
| } |
| |
| if (var_property.property & VAR_CHECK_VARIABLE_PROPERTY_READ_ONLY) { |
| ret = EFI_WRITE_PROTECTED; |
| goto out; |
| } |
| |
| /* Fill in contents */ |
| memcpy(&var_acc->guid, vendor, sizeof(var_acc->guid)); |
| var_acc->data_size = data_size; |
| var_acc->name_size = name_size; |
| var_acc->attr = attributes; |
| memcpy(var_acc->name, name, name_size); |
| memcpy((u8 *)var_acc->name + name_size, data, data_size); |
| |
| ret = mm_communicate(comm_buf, payload_size); |
| dev_dbg(pvt_data.dev, "Set Variable %s %d %lx\n", __FILE__, __LINE__, ret); |
| out: |
| kfree(comm_buf); |
| return ret; |
| } |
| |
| static efi_status_t tee_set_variable_nonblocking(efi_char16_t *name, |
| efi_guid_t *vendor, |
| u32 attributes, |
| unsigned long data_size, |
| void *data) |
| { |
| return EFI_UNSUPPORTED; |
| } |
| |
| static efi_status_t tee_query_variable_info(u32 attributes, |
| u64 *max_variable_storage_size, |
| u64 *remain_variable_storage_size, |
| u64 *max_variable_size) |
| { |
| struct smm_variable_query_info *mm_query_info; |
| size_t payload_size; |
| efi_status_t ret; |
| u8 *comm_buf; |
| |
| payload_size = sizeof(*mm_query_info); |
| mm_query_info = setup_mm_hdr(&comm_buf, payload_size, |
| SMM_VARIABLE_FUNCTION_QUERY_VARIABLE_INFO, |
| &ret); |
| if (!mm_query_info) |
| return EFI_OUT_OF_RESOURCES; |
| |
| mm_query_info->attr = attributes; |
| ret = mm_communicate(comm_buf, payload_size); |
| if (ret != EFI_SUCCESS) |
| goto out; |
| *max_variable_storage_size = mm_query_info->max_variable_storage; |
| *remain_variable_storage_size = |
| mm_query_info->remaining_variable_storage; |
| *max_variable_size = mm_query_info->max_variable_size; |
| |
| out: |
| kfree(comm_buf); |
| return ret; |
| } |
| |
| static void tee_stmm_efi_close_context(void *data) |
| { |
| tee_client_close_context(pvt_data.ctx); |
| } |
| |
| static void tee_stmm_efi_close_session(void *data) |
| { |
| tee_client_close_session(pvt_data.ctx, pvt_data.session); |
| } |
| |
| static void tee_stmm_restore_efivars_generic_ops(void) |
| { |
| efivars_unregister(&tee_efivars); |
| efivars_generic_ops_register(); |
| } |
| |
| static int tee_stmm_efi_probe(struct device *dev) |
| { |
| struct tee_ioctl_open_session_arg sess_arg; |
| efi_status_t ret; |
| int rc; |
| |
| pvt_data.ctx = tee_client_open_context(NULL, tee_ctx_match, NULL, NULL); |
| if (IS_ERR(pvt_data.ctx)) |
| return -ENODEV; |
| |
| rc = devm_add_action_or_reset(dev, tee_stmm_efi_close_context, NULL); |
| if (rc) |
| return rc; |
| |
| /* Open session with StMM PTA */ |
| memset(&sess_arg, 0, sizeof(sess_arg)); |
| export_uuid(sess_arg.uuid, &tee_stmm_efi_id_table[0].uuid); |
| rc = tee_client_open_session(pvt_data.ctx, &sess_arg, NULL); |
| if ((rc < 0) || (sess_arg.ret != 0)) { |
| dev_err(dev, "tee_client_open_session failed, err: %x\n", |
| sess_arg.ret); |
| return -EINVAL; |
| } |
| pvt_data.session = sess_arg.session; |
| pvt_data.dev = dev; |
| rc = devm_add_action_or_reset(dev, tee_stmm_efi_close_session, NULL); |
| if (rc) |
| return rc; |
| |
| ret = get_max_payload(&max_payload_size); |
| if (ret != EFI_SUCCESS) |
| return -EIO; |
| |
| max_buffer_size = MM_COMMUNICATE_HEADER_SIZE + |
| MM_VARIABLE_COMMUNICATE_SIZE + |
| max_payload_size; |
| |
| tee_efivar_ops.get_variable = tee_get_variable; |
| tee_efivar_ops.get_next_variable = tee_get_next_variable; |
| tee_efivar_ops.set_variable = tee_set_variable; |
| tee_efivar_ops.set_variable_nonblocking = tee_set_variable_nonblocking; |
| tee_efivar_ops.query_variable_store = efi_query_variable_store; |
| tee_efivar_ops.query_variable_info = tee_query_variable_info; |
| |
| efivars_generic_ops_unregister(); |
| pr_info("Using TEE-based EFI runtime variable services\n"); |
| efivars_register(&tee_efivars, &tee_efivar_ops); |
| |
| return 0; |
| } |
| |
| static int tee_stmm_efi_remove(struct device *dev) |
| { |
| tee_stmm_restore_efivars_generic_ops(); |
| |
| return 0; |
| } |
| |
| MODULE_DEVICE_TABLE(tee, tee_stmm_efi_id_table); |
| |
| static struct tee_client_driver tee_stmm_efi_driver = { |
| .id_table = tee_stmm_efi_id_table, |
| .driver = { |
| .name = "tee-stmm-efi", |
| .bus = &tee_bus_type, |
| .probe = tee_stmm_efi_probe, |
| .remove = tee_stmm_efi_remove, |
| }, |
| }; |
| |
| static int __init tee_stmm_efi_mod_init(void) |
| { |
| return driver_register(&tee_stmm_efi_driver.driver); |
| } |
| |
| static void __exit tee_stmm_efi_mod_exit(void) |
| { |
| driver_unregister(&tee_stmm_efi_driver.driver); |
| } |
| |
| module_init(tee_stmm_efi_mod_init); |
| module_exit(tee_stmm_efi_mod_exit); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_AUTHOR("Ilias Apalodimas <ilias.apalodimas@linaro.org>"); |
| MODULE_AUTHOR("Masahisa Kojima <masahisa.kojima@linaro.org>"); |
| MODULE_DESCRIPTION("TEE based EFI runtime variable service driver"); |