// SPDX-License-Identifier: GPL-2.0+ /* * EFI variable service via TEE * * Copyright (C) 2022 Linaro */ #include #include #include #include #include #include #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 "); MODULE_AUTHOR("Masahisa Kojima "); MODULE_DESCRIPTION("TEE based EFI runtime variable service driver");