summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPatrick Rudolph <patrick.rudolph@9elements.com>2021-04-21 10:02:55 +0200
committerLean Sheng Tan <sheng.tan@9elements.com>2023-04-03 21:13:54 +0000
commit1ab8ad66d450094a1e429a004fc10e1d4b32bbfb (patch)
tree8ab10999da640e9a13e0c2e227e6975c7750884e
parent389e73a97b34a7565c9354a9e73f26e330675854 (diff)
downloadcoreboot-1ab8ad66d450094a1e429a004fc10e1d4b32bbfb.tar.gz
coreboot-1ab8ad66d450094a1e429a004fc10e1d4b32bbfb.tar.bz2
coreboot-1ab8ad66d450094a1e429a004fc10e1d4b32bbfb.zip
drivers/efi: Add EFI variable store option support
Add a driver to read and write EFI variables stored in a region device. This is particularly useful for EDK2 as payload and allows to reuse existing EFI tools to set/get options used by the firmware. The write implementation is fault tolerant and doesn't corrupt the variable store. A faulting write might result in using the old value even though a 'newer' had been completely written. Implemented basic unit tests for header corruption, writing existing data and append new data into the store. Initial firmware region state: Initially the variable store region isn't formatted. Usually this is done in the EDK2 payload when no valid firmware volume could be found. It might be useful to do this offline or in coreboot to have a working option store on the first boot or when it was corrupted. Performance improvements: Right now the code always checks if the firmware volume header is valid. This could be optimised by caching the test result in heap. For write operations it would be good to cache the end of the variable store in the heap as well, instead of walking the whole store. For read operations caching the entire store could be considered. Reclaiming memory: The EFI variable store is append write only. To update an existing variable, first a new is written to the end of the store and then the previous is marked invalid. This only works on PNOR flash that allow to clear set bits, but keep cleared bits state. This mechanisms allows a fault tolerant write, but it also requires to "clean" the variable store for time to time. This cleaning would remove variables that have been marked "deleted". Such cleaning mechanism in turn must be fault tolerant and thus must use a second partition in the SPI flash as backup/working region. For now to cleaning is done in coreboot. Fault checking: The driver should check if a previous write was successful and if not mark variables as deleted on the next operation. Tested and working: - Enumerate all existing variables - Read variables - Write variables Change-Id: I8079f71d29da5dc2db956fc68bef1486fe3906bb Signed-off-by: Patrick Rudolph <patrick.rudolph@9elements.com> Reviewed-on: https://review.coreboot.org/c/coreboot/+/52564 Reviewed-by: Lean Sheng Tan <sheng.tan@9elements.com> Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
-rw-r--r--src/drivers/efi/Kconfig6
-rw-r--r--src/drivers/efi/Makefile.inc5
-rw-r--r--src/drivers/efi/efivars.c654
-rw-r--r--src/drivers/efi/efivars.h47
-rw-r--r--tests/Makefile.inc6
-rw-r--r--tests/drivers/Makefile.inc14
-rw-r--r--tests/drivers/efivars.c201
7 files changed, 930 insertions, 3 deletions
diff --git a/src/drivers/efi/Kconfig b/src/drivers/efi/Kconfig
new file mode 100644
index 000000000000..192a9176a008
--- /dev/null
+++ b/src/drivers/efi/Kconfig
@@ -0,0 +1,6 @@
+config DRIVERS_EFI_VARIABLE_STORE
+ bool "Include EFI variable store driver"
+ depends on UDK_BASE
+ help
+ Adds a driver that is able to read and write an EFI formatted
+ VariableStore as used by tianocore.
diff --git a/src/drivers/efi/Makefile.inc b/src/drivers/efi/Makefile.inc
new file mode 100644
index 000000000000..e2251b2c2b87
--- /dev/null
+++ b/src/drivers/efi/Makefile.inc
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+all-$(CONFIG_DRIVERS_EFI_VARIABLE_STORE) += efivars.c
+
+all-$(CONFIG_USE_UEFI_VARIABLE_STORE) += option.c
diff --git a/src/drivers/efi/efivars.c b/src/drivers/efi/efivars.c
new file mode 100644
index 000000000000..9a3233f11c15
--- /dev/null
+++ b/src/drivers/efi/efivars.c
@@ -0,0 +1,654 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <console/console.h>
+
+#include <vendorcode/intel/edk2/UDK2017/MdePkg/Include/Uefi/UefiBaseType.h>
+#include <vendorcode/intel/edk2/UDK2017/MdePkg/Include/Uefi/UefiMultiPhase.h>
+#include <vendorcode/intel/edk2/UDK2017/MdePkg/Include/Pi/PiFirmwareVolume.h>
+#include <vendorcode/intel/edk2/UDK2017/MdeModulePkg/Include/Guid/VariableFormat.h>
+#include <lib.h>
+
+#include "efivars.h"
+
+#define PREFIX "EFIVARS: "
+
+static const EFI_GUID EfiVariableGuid = {
+ 0xddcf3616, 0x3275, 0x4164, { 0x98, 0xb6, 0xfe, 0x85, 0x70, 0x7f, 0xfe, 0x7d } };
+static const EFI_GUID EfiAuthenticatedVariableGuid = {
+ 0xaaf32c78, 0x947b, 0x439a, { 0xa1, 0x80, 0x2e, 0x14, 0x4e, 0xc3, 0x77, 0x92 } };
+static const EFI_GUID EfiSystemNvDataFvGuid = {
+ 0xfff12b8d, 0x7696, 0x4c8b, { 0xa9, 0x85, 0x27, 0x47, 0x07, 0x5b, 0x4f, 0x50 } };
+
+static void print_guid(int log_level, const EFI_GUID *g)
+{
+ printk(log_level, "GUID: %08x-%04x-%04x-%02x%02x%02x%02x%02x%02x%02x%02x",
+ g->Data1, g->Data2, g->Data3, g->Data4[0], g->Data4[1], g->Data4[2],
+ g->Data4[3], g->Data4[4], g->Data4[5], g->Data4[6], g->Data4[7]);
+
+}
+
+static bool compare_guid(const EFI_GUID *a, const EFI_GUID *b)
+{
+ return memcmp(a, b, sizeof(*a)) == 0;
+}
+
+/* Reads the CHAR16 string from rdev at offset and prints it */
+static enum cb_err rdev_print_wchar(int log_level, struct region_device *rdev, size_t offset)
+{
+ CHAR16 c;
+ int i = 0;
+
+ /* Convert ASCII to UTF-16 */
+ do {
+ if (rdev_readat(rdev, &c, offset + i * sizeof(c), sizeof(c)) != sizeof(c))
+ return CB_EFI_ACCESS_ERROR;
+ if (c < 0x80)
+ printk(log_level, "%c", (char)c);
+ else
+ printk(log_level, "\\u%04x", c);
+
+ i++;
+ } while (c);
+ return CB_SUCCESS;
+}
+
+/* Convert an ASCII string to UTF-16 and write it to the rdev starting at offset. */
+static enum cb_err rdev_write_wchar(struct region_device *rdev, size_t offset, const char *msg)
+{
+ size_t i;
+ CHAR16 c;
+
+ /* Convert ASCII to UTF-16 */
+ for (i = 0; i < strlen(msg) + 1; i++) {
+ c = msg[i];
+
+ if (rdev_writeat(rdev, &c, offset + i * sizeof(c), sizeof(c)) != sizeof(c))
+ return CB_EFI_ACCESS_ERROR;
+ }
+ return CB_SUCCESS;
+}
+
+/* Read an UTF-16 string from rdev at offset and compare it to ASCII string */
+static int rdev_strcmp_wchar_ascii(struct region_device *rdev, size_t offset, const char *msg)
+{
+ size_t i;
+ CHAR16 c;
+ int r;
+
+ i = 0;
+ /* Compare UTF-16 and ASCII */
+ while (1) {
+ if (rdev_readat(rdev, &c, offset + i * sizeof(c), sizeof(c)) != sizeof(c))
+ return CB_EFI_ACCESS_ERROR;
+ if ((r = (c - msg[i])) != 0 || !c)
+ break;
+
+ i++;
+ }
+ return r;
+}
+
+/* Compare an rdev region and a data buffer */
+static int rdev_memcmp(struct region_device *rdev, size_t offset, uint8_t *data, size_t size)
+{
+ uint8_t buf[16];
+ size_t i;
+ int r;
+
+ i = 0;
+ while (size >= sizeof(buf)) {
+ if (rdev_readat(rdev, buf, offset + i, sizeof(buf)) != sizeof(buf))
+ return CB_EFI_ACCESS_ERROR;
+ r = memcmp(buf, data + i, sizeof(buf));
+ if (r != 0)
+ return r;
+ i += sizeof(buf);
+ size -= sizeof(buf);
+ }
+ while (size > 0) {
+ if (rdev_readat(rdev, buf, offset + i, 1) != 1)
+ return CB_EFI_ACCESS_ERROR;
+ r = buf[0] - data[i];
+ if (r != 0)
+ return r;
+ i++;
+ size--;
+ }
+ return 0;
+}
+
+
+static enum cb_err validate_fv_header(const struct region_device *rdev,
+ EFI_FIRMWARE_VOLUME_HEADER *fw_vol_hdr)
+{
+ uint16_t checksum, data;
+ size_t i;
+
+ if (rdev_readat(rdev, fw_vol_hdr, 0, sizeof(*fw_vol_hdr)) != sizeof(*fw_vol_hdr))
+ return CB_EFI_ACCESS_ERROR;
+
+ /*
+ * Verify the header revision, header signature, length
+ * Length of FvBlock cannot be 2**64-1
+ * HeaderLength cannot be an odd number
+ */
+ if ((fw_vol_hdr->Revision != EFI_FVH_REVISION)
+ || (fw_vol_hdr->Signature != EFI_FVH_SIGNATURE)
+ || (fw_vol_hdr->FvLength > region_device_sz(rdev))
+ || (fw_vol_hdr->HeaderLength > region_device_sz(rdev))
+ || (fw_vol_hdr->HeaderLength & 1)) {
+ printk(BIOS_WARNING, PREFIX "No Firmware Volume header present\n");
+ return CB_EFI_FVH_INVALID;
+ }
+
+ /* Check the Firmware Volume Guid */
+ if (!compare_guid(&fw_vol_hdr->FileSystemGuid, &EfiSystemNvDataFvGuid)) {
+ printk(BIOS_WARNING, PREFIX "Firmware Volume Guid non-compatible\n");
+ return CB_EFI_FVH_INVALID;
+ }
+
+ /* Verify the header checksum */
+ checksum = 0;
+ for (i = 0; i < fw_vol_hdr->HeaderLength; i += 2) {
+ if (rdev_readat(rdev, &data, i, sizeof(data)) != sizeof(data))
+ return CB_EFI_ACCESS_ERROR;
+ checksum = (uint16_t)(checksum + data); /* intentionally overflows */
+ }
+ if (checksum != 0) {
+ printk(BIOS_WARNING, PREFIX "FV checksum is invalid: 0x%X\n", checksum);
+ return CB_EFI_CHECKSUM_INVALID;
+ }
+
+ printk(BIOS_SPEW, PREFIX "UEFI FV with size %lld found\n", fw_vol_hdr->FvLength);
+
+ return CB_SUCCESS;
+
+}
+
+static enum cb_err
+validate_variable_store_header(const EFI_FIRMWARE_VOLUME_HEADER *fv_hdr,
+ struct region_device *rdev,
+ bool *auth_format)
+{
+ VARIABLE_STORE_HEADER hdr;
+ size_t length;
+
+ if (rdev_readat(rdev, &hdr, fv_hdr->HeaderLength, sizeof(hdr)) != sizeof(hdr))
+ return CB_EFI_ACCESS_ERROR;
+
+ /* Check the Variable Store Guid */
+ if (!compare_guid(&hdr.Signature, &EfiVariableGuid) &&
+ !compare_guid(&hdr.Signature, &EfiAuthenticatedVariableGuid)) {
+ printk(BIOS_WARNING, PREFIX "Variable Store Guid non-compatible\n");
+ return CB_EFI_VS_CORRUPTED_INVALID;
+ }
+
+ *auth_format = compare_guid(&hdr.Signature, &EfiAuthenticatedVariableGuid);
+
+ length = region_device_sz(rdev) - fv_hdr->HeaderLength;
+ if (hdr.Size > length) {
+ printk(BIOS_WARNING, PREFIX "Variable Store Length does not match\n");
+ return CB_EFI_VS_CORRUPTED_INVALID;
+ }
+
+ if (hdr.Format != VARIABLE_STORE_FORMATTED)
+ return CB_EFI_VS_NOT_FORMATTED_INVALID;
+
+ if (hdr.State != VARIABLE_STORE_HEALTHY)
+ return CB_EFI_VS_CORRUPTED_INVALID;
+
+ if (rdev_chain(rdev, rdev, fv_hdr->HeaderLength + sizeof(hdr), hdr.Size)) {
+ printk(BIOS_WARNING, PREFIX "rdev_chain failed\n");
+ return CB_EFI_ACCESS_ERROR;
+ }
+
+ printk(BIOS_SPEW, PREFIX "UEFI variable store with size %zu found\n",
+ region_device_sz(rdev));
+
+ return CB_SUCCESS;
+}
+
+struct efi_find_args {
+ const EFI_GUID *guid;
+ const char *name;
+ uint32_t *size;
+ void *data;
+};
+
+static bool match(struct region_device *rdev, VARIABLE_HEADER *hdr, size_t hdr_size,
+ const char *name, const EFI_GUID *guid)
+{
+ /* Only search for valid or in transition to be deleted variables */
+ if ((hdr->State != VAR_ADDED) &&
+ (hdr->State != (VAR_IN_DELETED_TRANSITION & VAR_ADDED)))
+ return false;
+
+ if ((!compare_guid(&hdr->VendorGuid, guid)) ||
+ !hdr->NameSize ||
+ !hdr->DataSize)
+ return false;
+
+ if (rdev_strcmp_wchar_ascii(rdev, hdr_size, name) != 0)
+ return false;
+
+ return true;
+}
+
+static
+enum cb_err find_and_copy(struct region_device *rdev, VARIABLE_HEADER *hdr, size_t hdr_size,
+ void *arg, bool *stop)
+{
+ struct efi_find_args *fa = (struct efi_find_args *)arg;
+
+ if (!match(rdev, hdr, hdr_size, fa->name, fa->guid))
+ return CB_SUCCESS;
+
+ *stop = true;
+ if (*(fa->size) < hdr->DataSize)
+ return CB_EFI_BUFFER_TOO_SMALL;
+
+ if (rdev_readat(rdev, fa->data, hdr_size + hdr->NameSize, hdr->DataSize) !=
+ hdr->DataSize)
+ return CB_EFI_ACCESS_ERROR;
+
+ *(fa->size) = hdr->DataSize;
+ return CB_SUCCESS;
+}
+
+struct efi_find_compare_args {
+ const EFI_GUID *guid;
+ const char *name;
+ uint32_t size;
+ void *data;
+ bool match;
+};
+
+static
+enum cb_err find_and_compare(struct region_device *rdev, VARIABLE_HEADER *hdr, size_t hdr_size,
+ void *arg, bool *stop)
+{
+ struct efi_find_compare_args *fa = (struct efi_find_compare_args *)arg;
+
+ if (!match(rdev, hdr, hdr_size, fa->name, fa->guid))
+ return CB_SUCCESS;
+
+ *stop = true;
+ if (fa->size != hdr->DataSize) {
+ fa->match = false;
+ return CB_SUCCESS;
+ }
+
+ fa->match = rdev_memcmp(rdev, hdr_size + hdr->NameSize, fa->data, hdr->DataSize) == 0;
+
+ return CB_SUCCESS;
+}
+
+static enum cb_err noop(struct region_device *rdev, VARIABLE_HEADER *hdr, size_t hdr_size,
+ void *arg, bool *stop)
+{
+ /* Does nothing. */
+ return CB_SUCCESS;
+}
+
+static enum cb_err print_var(struct region_device *rdev, VARIABLE_HEADER *hdr, size_t hdr_size,
+ void *arg, bool *stop)
+{
+ uint8_t buf[16];
+ size_t len, i;
+
+ printk(BIOS_DEBUG, "%08zx: Var ", region_device_offset(rdev));
+ print_guid(BIOS_DEBUG, &hdr->VendorGuid);
+
+ printk(BIOS_DEBUG, "-");
+
+ rdev_print_wchar(BIOS_DEBUG, rdev, hdr_size);
+
+ printk(BIOS_DEBUG, ", State %02x, Size %02x\n", hdr->State, hdr->DataSize);
+
+ if (hdr->DataSize && hdr->NameSize) {
+ len = sizeof(buf) < hdr->DataSize ? sizeof(buf) : hdr->DataSize;
+ if (rdev_readat(rdev, buf, hdr_size + hdr->NameSize, len) != len)
+ return CB_EFI_ACCESS_ERROR;
+ printk(BIOS_DEBUG, " Data: ");
+
+ for (i = 0; i < len; i++)
+ printk(BIOS_DEBUG, "0x%02x ", buf[i]);
+
+ if (hdr->DataSize > len)
+ printk(BIOS_DEBUG, "...");
+
+ printk(BIOS_DEBUG, "\n");
+ }
+
+ return CB_SUCCESS;
+}
+
+static enum cb_err walk_variables(struct region_device *rdev,
+ bool auth_format,
+ enum cb_err (*walker)(struct region_device *rdev,
+ VARIABLE_HEADER *hdr,
+ size_t hdr_size,
+ void *arg,
+ bool *stop),
+ void *walker_arg)
+{
+ AUTHENTICATED_VARIABLE_HEADER auth_hdr;
+ size_t header_size, var_size;
+ VARIABLE_HEADER hdr;
+ bool stop;
+ enum cb_err ret;
+
+ if (auth_format)
+ header_size = sizeof(AUTHENTICATED_VARIABLE_HEADER);
+ else
+ header_size = sizeof(VARIABLE_HEADER);
+
+ do {
+ if (auth_format) {
+ if (rdev_readat(rdev, &auth_hdr, 0, sizeof(auth_hdr))
+ != sizeof(auth_hdr))
+ return CB_EFI_ACCESS_ERROR;
+ hdr.Reserved = auth_hdr.Reserved;
+ hdr.StartId = auth_hdr.StartId;
+ hdr.State = auth_hdr.State;
+ hdr.Attributes = auth_hdr.Attributes;
+ hdr.NameSize = auth_hdr.NameSize;
+ hdr.DataSize = auth_hdr.DataSize;
+ memcpy(&hdr.VendorGuid, &auth_hdr.VendorGuid, sizeof(hdr.VendorGuid));
+ } else if (rdev_readat(rdev, &hdr, 0, sizeof(hdr)) != sizeof(hdr)) {
+ return CB_EFI_ACCESS_ERROR;
+ }
+ if (hdr.StartId != VARIABLE_DATA)
+ break;
+
+ if (hdr.State == UINT8_MAX ||
+ hdr.DataSize == UINT32_MAX ||
+ hdr.NameSize == UINT32_MAX ||
+ hdr.Attributes == UINT32_MAX) {
+ hdr.NameSize = 0;
+ hdr.DataSize = 0;
+ }
+
+ printk(BIOS_SPEW, "Found variable with state %02x and ", hdr.State);
+ print_guid(BIOS_SPEW, &hdr.VendorGuid);
+ printk(BIOS_SPEW, "\n");
+
+ stop = false;
+
+ ret = walker(rdev, &hdr, header_size, walker_arg, &stop);
+
+ if (ret != CB_SUCCESS || stop)
+ return ret;
+
+ var_size = ALIGN_UP(header_size + hdr.NameSize + hdr.DataSize,
+ HEADER_ALIGNMENT);
+ } while (!rdev_chain(rdev, rdev, var_size, region_device_sz(rdev) - var_size));
+
+ return CB_EFI_OPTION_NOT_FOUND;
+}
+
+static enum cb_err efi_fv_init(struct region_device *rdev, bool *auth_format)
+{
+ EFI_FIRMWARE_VOLUME_HEADER fv_hdr;
+ enum cb_err ret;
+
+ ret = validate_fv_header(rdev, &fv_hdr);
+ if (ret != CB_SUCCESS) {
+ printk(BIOS_WARNING, PREFIX "Failed to validate firmware header\n");
+
+ return ret;
+ }
+ ret = validate_variable_store_header(&fv_hdr, rdev, auth_format);
+ if (ret != CB_SUCCESS)
+ printk(BIOS_WARNING, PREFIX "Failed to validate variable store header\n");
+
+ return ret;
+}
+
+enum cb_err efi_fv_print_options(struct region_device *rdev)
+{
+ enum cb_err ret;
+ bool auth_format;
+
+ ret = efi_fv_init(rdev, &auth_format);
+ if (ret != CB_SUCCESS)
+ return ret;
+
+ return walk_variables(rdev, auth_format, print_var, NULL);
+}
+
+/*
+ * efi_fv_get_option
+ * - writes up to *size bytes into a buffer pointed to by *dest
+ * - rdev is the spi flash region to operate on
+ * - the FVH and variable store header must have been initialized by a third party
+ */
+enum cb_err efi_fv_get_option(struct region_device *rdev,
+ const EFI_GUID *guid,
+ const char *name,
+ void *dest,
+ uint32_t *size)
+{
+ struct efi_find_args args;
+ bool auth_format;
+ enum cb_err ret;
+
+ ret = efi_fv_init(rdev, &auth_format);
+ if (ret != CB_SUCCESS)
+ return ret;
+
+ args.guid = guid;
+ args.name = name;
+ args.size = size;
+ args.data = dest;
+
+ return walk_variables(rdev, auth_format, find_and_copy, &args);
+}
+
+static enum cb_err write_auth_hdr(struct region_device *rdev, const EFI_GUID *guid,
+ const char *name, void *data, size_t size)
+{
+ AUTHENTICATED_VARIABLE_HEADER auth_hdr;
+ size_t name_size, var_size;
+ enum cb_err ret;
+
+ name_size = (strlen(name) + 1) * sizeof(CHAR16);
+ var_size = name_size + size + sizeof(auth_hdr);
+
+ if (var_size > region_device_sz(rdev))
+ return CB_EFI_STORE_FULL;
+
+ /* Sanity check. flash must be blank */
+ if (rdev_readat(rdev, &auth_hdr, 0, sizeof(auth_hdr)) != sizeof(auth_hdr))
+ return CB_EFI_ACCESS_ERROR;
+
+ if (auth_hdr.StartId != UINT16_MAX ||
+ auth_hdr.State != UINT8_MAX ||
+ auth_hdr.DataSize != UINT32_MAX ||
+ auth_hdr.NameSize != UINT32_MAX ||
+ auth_hdr.Attributes != UINT32_MAX) {
+ return CB_EFI_ACCESS_ERROR;
+ }
+
+ memset(&auth_hdr, 0xff, sizeof(auth_hdr));
+
+ auth_hdr.StartId = VARIABLE_DATA;
+ auth_hdr.Attributes = EFI_VARIABLE_NON_VOLATILE|
+ EFI_VARIABLE_BOOTSERVICE_ACCESS|
+ EFI_VARIABLE_RUNTIME_ACCESS;
+ auth_hdr.NameSize = name_size;
+ auth_hdr.DataSize = size;
+ memcpy(&auth_hdr.VendorGuid, guid, sizeof(EFI_GUID));
+
+ /* Write header with no State */
+ if (rdev_writeat(rdev, &auth_hdr, 0, sizeof(auth_hdr)) != sizeof(auth_hdr))
+ return CB_EFI_ACCESS_ERROR;
+
+ /* Set header State to valid header */
+ auth_hdr.State = VAR_HEADER_VALID_ONLY;
+ if (rdev_writeat(rdev, &auth_hdr.State, offsetof(AUTHENTICATED_VARIABLE_HEADER, State),
+ sizeof(auth_hdr.State)) != sizeof(auth_hdr.State))
+ return CB_EFI_ACCESS_ERROR;
+
+ /* Write the name */
+ ret = rdev_write_wchar(rdev, sizeof(auth_hdr), name);
+ if (ret != CB_SUCCESS)
+ return ret;
+
+ /* Write the data */
+ if (rdev_writeat(rdev, data, sizeof(auth_hdr) + name_size, size) != size)
+ return CB_EFI_ACCESS_ERROR;
+
+ /* Set header State to valid data */
+ auth_hdr.State = VAR_ADDED;
+ if (rdev_writeat(rdev, &auth_hdr.State, offsetof(AUTHENTICATED_VARIABLE_HEADER, State),
+ sizeof(auth_hdr.State)) != sizeof(auth_hdr.State))
+ return CB_EFI_ACCESS_ERROR;
+
+ return CB_SUCCESS;
+}
+
+static enum cb_err write_hdr(struct region_device *rdev, const EFI_GUID *guid,
+ const char *name,
+ void *data,
+ size_t size)
+{
+ VARIABLE_HEADER hdr;
+ size_t name_size, var_size;
+ enum cb_err ret;
+
+ name_size = (strlen(name) + 1) * sizeof(CHAR16);
+ var_size = name_size + size + sizeof(hdr);
+ if (var_size > region_device_sz(rdev))
+ return CB_EFI_STORE_FULL;
+
+ /* Sanity check. flash must be blank */
+ if (rdev_readat(rdev, &hdr, 0, sizeof(hdr)) != sizeof(hdr))
+ return CB_EFI_ACCESS_ERROR;
+
+ if (hdr.StartId != UINT16_MAX ||
+ hdr.State != UINT8_MAX ||
+ hdr.DataSize != UINT32_MAX ||
+ hdr.NameSize != UINT32_MAX ||
+ hdr.Attributes != UINT32_MAX) {
+ return CB_EFI_ACCESS_ERROR;
+ }
+
+ memset(&hdr, 0xff, sizeof(hdr));
+
+ hdr.StartId = VARIABLE_DATA;
+ hdr.Attributes = EFI_VARIABLE_NON_VOLATILE|
+ EFI_VARIABLE_BOOTSERVICE_ACCESS|
+ EFI_VARIABLE_RUNTIME_ACCESS;
+ hdr.NameSize = name_size;
+ hdr.DataSize = size;
+ memcpy(&hdr.VendorGuid, guid, sizeof(EFI_GUID));
+
+ /* Write header with no State */
+ if (rdev_writeat(rdev, &hdr, 0, sizeof(hdr)) != sizeof(hdr))
+ return CB_EFI_ACCESS_ERROR;
+
+ /* Set header State to valid header */
+ hdr.State = VAR_HEADER_VALID_ONLY;
+ if (rdev_writeat(rdev, &hdr.State, offsetof(VARIABLE_HEADER, State),
+ sizeof(hdr.State)) != sizeof(hdr.State))
+ return CB_EFI_ACCESS_ERROR;
+
+ /* Write the name */
+ ret = rdev_write_wchar(rdev, sizeof(hdr), name);
+ if (ret != CB_SUCCESS)
+ return ret;
+
+ /* Write the data */
+ if (rdev_writeat(rdev, data, sizeof(hdr) + name_size, size) != size)
+ return CB_EFI_ACCESS_ERROR;
+
+ /* Set header State to valid data */
+ hdr.State = VAR_ADDED;
+ if (rdev_writeat(rdev, &hdr.State, offsetof(VARIABLE_HEADER, State),
+ sizeof(hdr.State)) != sizeof(hdr.State))
+ return CB_EFI_ACCESS_ERROR;
+
+ return CB_SUCCESS;
+}
+
+/*
+ * efi_fv_set_option
+ * - writes size bytes read from the buffer pointed to by *data
+ * - rdev is the spi flash region to operate on
+ * - the FVH and variable store header must have been initialized by a third party
+ */
+enum cb_err efi_fv_set_option(struct region_device *rdev,
+ const EFI_GUID *guid,
+ const char *name,
+ void *data,
+ uint32_t size)
+{
+ struct region_device rdev_old;
+ struct efi_find_compare_args args;
+ bool found_existing;
+ VARIABLE_HEADER hdr;
+ bool auth_format;
+ enum cb_err ret;
+
+ ret = efi_fv_init(rdev, &auth_format);
+ if (ret != CB_SUCCESS)
+ return ret;
+
+ /* Find existing variable */
+ args.guid = guid;
+ args.name = name;
+ args.size = size;
+ args.match = false;
+ args.data = data;
+
+ ret = walk_variables(rdev, auth_format, find_and_compare, &args);
+ found_existing = ret == CB_SUCCESS;
+
+ if (found_existing) {
+ printk(BIOS_ERR, "found existing variable %s, match =%d\n", name, args.match);
+
+ if (args.match)
+ return CB_SUCCESS;
+
+ rdev_old = *rdev;
+
+ /* Mark as to be deleted */
+ hdr.State = VAR_IN_DELETED_TRANSITION;
+ if (rdev_writeat(rdev, &hdr.State, offsetof(VARIABLE_HEADER, State),
+ sizeof(hdr.State)) != sizeof(hdr.State))
+ return CB_EFI_ACCESS_ERROR;
+ }
+
+ /* Walk to end of variable store */
+ ret = walk_variables(rdev, auth_format, noop, NULL);
+ if (ret != CB_EFI_OPTION_NOT_FOUND)
+ return ret;
+
+ /* Now append new variable:
+ * 1. Write the header without State field.
+ * 2. Write the State field and set it to HEADER_VALID.
+ * 3. Write data
+ * 4. Write the State field and set it to VAR_ADDED
+ */
+
+ if (auth_format)
+ ret = write_auth_hdr(rdev, guid, name, data, size);
+ else
+ ret = write_hdr(rdev, guid, name, data, size);
+ if (ret != CB_SUCCESS)
+ return ret;
+
+ if (found_existing) {
+ /* Mark old variable as deleted */
+ hdr.State = VAR_DELETED;
+ if (rdev_writeat(&rdev_old, &hdr.State, offsetof(VARIABLE_HEADER, State),
+ sizeof(hdr.State)) != sizeof(hdr.State))
+ return CB_EFI_ACCESS_ERROR;
+ }
+
+ return CB_SUCCESS;
+}
diff --git a/src/drivers/efi/efivars.h b/src/drivers/efi/efivars.h
new file mode 100644
index 000000000000..b3443b33b350
--- /dev/null
+++ b/src/drivers/efi/efivars.h
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef _EDK2_OPTION_H_
+#define _EDK2_OPTION_H_
+
+#include <types.h>
+#include <commonlib/region.h>
+
+#include <vendorcode/intel/edk2/UDK2017/MdePkg/Include/Uefi/UefiBaseType.h>
+
+/**
+ * efi_fv_get_option
+ * Use the provided EFI variable store inside the region device as variable store.
+ * @rdev: the readable region to operate on
+ * @guid: the vendor guid to look for
+ * @name: the variable name to look for. NULL terminated.
+ * @dest: memory buffer to place the result into
+ * @size: on input the size of buffer pointed to by dest.
+ * on output the number of bytes written.
+ */
+
+enum cb_err efi_fv_get_option(struct region_device *rdev,
+ const EFI_GUID *guid,
+ const char *name,
+ void *dest,
+ uint32_t *size);
+
+/**
+ * efi_fv_set_option
+ * Use the provided EFI variable store inside the region device as variable store.
+ * If the variable exists with the same size and contents, nothing will be written
+ * to the region device.
+ * @rdev: the read/writable region to operate on
+ * @guid: the vendor guid to write
+ * @name: the variable name to write. NULL terminated.
+ * @data: memory buffer where to read data from
+ * @size: the size of buffer pointed to by data
+ */
+enum cb_err efi_fv_set_option(struct region_device *rdev,
+ const EFI_GUID *guid,
+ const char *name,
+ void *data,
+ uint32_t size);
+
+enum cb_err efi_fv_print_options(struct region_device *rdev);
+
+#endif /* _EDK2_OPTION_H_ */
diff --git a/tests/Makefile.inc b/tests/Makefile.inc
index 3c25bd6f8e15..584b4c0c9dae 100644
--- a/tests/Makefile.inc
+++ b/tests/Makefile.inc
@@ -29,9 +29,9 @@ stages += ramstage rmodule postcar libagesa
alltests :=
subdirs := tests/arch tests/acpi tests/commonlib tests/console tests/cpu
-subdirs += tests/device tests/drivers tests/ec tests/lib tests/mainboard
-subdirs += tests/northbridge tests/security tests/soc tests/southbridge
-subdirs += tests/superio tests/vendorcode
+subdirs += tests/device tests/drivers tests/ec tests/lib
+subdirs += tests/mainboard tests/northbridge tests/security tests/soc
+subdirs += tests/southbridge tests/superio tests/vendorcode
define tests-handler
alltests += $(1)$(2)
diff --git a/tests/drivers/Makefile.inc b/tests/drivers/Makefile.inc
new file mode 100644
index 000000000000..29b7ed7fdd1f
--- /dev/null
+++ b/tests/drivers/Makefile.inc
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+tests-y += efivars-test
+
+efivars-test-srcs += tests/drivers/efivars.c
+efivars-test-srcs += src/drivers/efi/efivars.c
+efivars-test-srcs += tests/stubs/console.c
+efivars-test-srcs += src/commonlib/region.c
+
+efivars-test-cflags += -I src/vendorcode/intel/edk2/UDK2017/MdePkg/Include/
+efivars-test-cflags += -I src/vendorcode/intel/edk2/UDK2017/MdePkg/Include/Ia32/
+efivars-test-cflags += -I src/vendorcode/intel/edk2/UDK2017/MdePkg/Include/Pi/
+efivars-test-cflags += -I src/vendorcode/intel/edk2/UDK2017/MdeModulePkg/Include/
+
diff --git a/tests/drivers/efivars.c b/tests/drivers/efivars.c
new file mode 100644
index 000000000000..6899ce5bbaf7
--- /dev/null
+++ b/tests/drivers/efivars.c
@@ -0,0 +1,201 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#include <drivers/efi/efivars.h>
+#include <limits.h>
+#include <vendorcode/intel/edk2/UDK2017/MdePkg/Include/Pi/PiFirmwareVolume.h>
+#include <vendorcode/intel/edk2/UDK2017/MdeModulePkg/Include/Guid/VariableFormat.h>
+#include <string.h>
+#include <tests/test.h>
+#include <types.h>
+
+/* Dummy firmware volume header for a 0x30000 byte partition with a single entry
+ * in a formatted variable store.
+ */
+static const uint8_t FVH[] = {
+ /* EFI_FIRMWARE_VOLUME_HEADER */
+ /* UINT8 ZeroVector[16] */
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ /* EFI_GUID FileSystemGuid */
+ 0x8d, 0x2b, 0xf1, 0xff, 0x96, 0x76, 0x8b, 0x4c, 0xa9, 0x85, 0x27, 0x47, 0x07, 0x5b,
+ 0x4f, 0x50,
+ /* UINT64 FvLength */
+ 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
+ /* UINT32 Signature */
+ 0x5f, 0x46, 0x56, 0x48,
+ /* EFI_FVB_ATTRIBUTES_2 Attributes */
+ 0x36, 0x0e, 0x00, 0x00,
+ /* UINT16 HeaderLength */
+ 0x48, 0x00,
+ /* UINT16 Checksum */
+ 0x00, 0xfa,
+ /* UINT16 ExtHeaderOffset */
+ 0x00, 0x00,
+ /* UINT8 Reserved[1] */
+ 0x00,
+ /* UINT8 Revision */
+ 0x02,
+ /* EFI_FV_BLOCK_MAP_ENTRY BlockMap[2] */
+ 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+
+ /* Variable Info Header */
+ /* EFI_GUID Signature */
+ 0x78, 0x2c, 0xf3, 0xaa, 0x7b, 0x94, 0x9a, 0x43, 0xa1, 0x80, 0x2e, 0x14, 0x4e, 0xc3,
+ 0x77, 0x92,
+ /* UINT32 Size */
+ 0xb8, 0xff, 0x00, 0x00,
+ /* UINT8 Format */
+ 0x5a,
+ /* UINT8 State */
+ 0xfe,
+ /* UINT16 Reserved */
+ 0x00, 0x00,
+ /* UINT32 Reserved1 */
+ 0x00, 0x00, 0x00, 0x00,
+ /* AUTHENTICATED_VARIABLE_HEADER */
+ /* UINT16 StartId */
+ 0xaa, 0x55,
+ /* UINT8 State */
+ 0x3f,
+ /* UINT8 Reserved */
+ 0xff,
+ /* UINT32 Attributes */
+ 0x07, 0x00, 0x00, 0x00,
+ /* UINT64 MonotonicCount */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ /* EFI_TIME TimeStamp */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff,
+ /* UINT32 PubKeyIndex */
+ 0xff, 0xff, 0xff, 0xff,
+ /* UINT32 NameSize */
+ 0x12, 0x00, 0x00, 0x00,
+ /* UINT32 DataSize */
+ 0x09, 0x00, 0x00, 0x00,
+ /* EFI_GUID VendorGuid */
+ 0x1d, 0x4c, 0xae, 0xce, 0x5b, 0x33, 0x85, 0x46, 0xa4, 0xa0, 0xfc, 0x4a,
+ 0x94, 0xee, 0xa0, 0x85,
+ /* L"coreboot" */
+ 0x63, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, 0x62, 0x00,
+ 0x6f, 0x00, 0x6f, 0x00, 0x74, 0x00, 0x00, 0x00,
+ /* "is great" */
+ 0x69, 0x73, 0x20, 0x67, 0x72, 0x65, 0x61, 0x74, 0x00,
+};
+
+#define FVH_CHECKSUMMED_SIZE (sizeof(EFI_FIRMWARE_VOLUME_HEADER) + 8 + sizeof(EFI_GUID))
+
+static struct region_device flash_rdev_rw;
+static uint8_t flash_buffer[0x30000];
+
+static const char *name = "coreboot";
+
+static void mock_rdev(bool init)
+{
+ if (init) {
+ /* Emulate NOR flash by setting all bits to 1 */
+ memset(flash_buffer, 0xff, sizeof(flash_buffer));
+ /* Place _FVH and VIH headers, as well as test data */
+ memcpy(flash_buffer, FVH, sizeof(FVH));
+ }
+
+ rdev_chain_mem_rw(&flash_rdev_rw, flash_buffer, sizeof(flash_buffer));
+}
+
+static const EFI_GUID EficorebootNvDataGuid = {
+ 0xceae4c1d, 0x335b, 0x4685, { 0xa4, 0xa0, 0xfc, 0x4a, 0x94, 0xee, 0xa0, 0x85 } };
+
+/* Test valid and corrupted FVH header */
+static void efi_test_header(void **state)
+{
+ enum cb_err ret;
+ uint8_t buf[16];
+ uint32_t size;
+ int i;
+
+ mock_rdev(true);
+
+ /* Test variable lookup with intact header */
+ size = sizeof(buf);
+ ret = efi_fv_get_option(&flash_rdev_rw, &EficorebootNvDataGuid, name, buf, &size);
+ assert_int_equal(ret, CB_SUCCESS);
+ assert_int_equal(size, strlen("is great")+1);
+ assert_string_equal((const char *)buf, "is great");
+
+ for (i = 0; i < FVH_CHECKSUMMED_SIZE; i++) {
+ mock_rdev(true);
+
+ /* Flip some bits */
+ flash_buffer[i] ^= 0xff;
+
+ size = sizeof(buf);
+ ret = efi_fv_get_option(&flash_rdev_rw, &EficorebootNvDataGuid, name, buf,
+ &size);
+ assert_int_not_equal(ret, CB_SUCCESS);
+ }
+}
+
+/* Write with the same key and value should not modify the store */
+static void efi_test_noop_existing_write(void **state)
+{
+ enum cb_err ret;
+ int i;
+
+ mock_rdev(true);
+
+ ret = efi_fv_set_option(&flash_rdev_rw,
+ &EficorebootNvDataGuid,
+ name,
+ "is great",
+ strlen("is great") + 1);
+
+ assert_int_equal(ret, CB_SUCCESS);
+
+ for (i = sizeof(FVH); i < sizeof(flash_buffer); i++)
+ assert_int_equal(flash_buffer[i], 0xff);
+}
+
+static void efi_test_new_write(void **state)
+{
+ enum cb_err ret;
+ uint8_t buf[16];
+ uint32_t size;
+ int i;
+
+ mock_rdev(true);
+
+ ret = efi_fv_set_option(&flash_rdev_rw, &EficorebootNvDataGuid,
+ name, "is awesome", strlen("is awesome") + 1);
+ assert_int_equal(ret, CB_SUCCESS);
+
+ /* New variable has been written */
+ assert_int_equal(flash_buffer[ALIGN_UP(sizeof(FVH), 4)], 0xaa);
+ assert_int_equal(flash_buffer[ALIGN_UP(sizeof(FVH), 4) + 1], 0x55);
+
+ /* Remaining space is blank */
+ for (i = ALIGN_UP(sizeof(FVH), 4) + 89; i < sizeof(flash_buffer); i++)
+ assert_int_equal(flash_buffer[i], 0xff);
+
+ mock_rdev(false);
+
+ memset(buf, 0, sizeof(buf));
+ size = sizeof(buf);
+ ret = efi_fv_get_option(&flash_rdev_rw, &EficorebootNvDataGuid, name, buf,
+ &size);
+ assert_int_equal(ret, CB_SUCCESS);
+ assert_int_equal(size, strlen("is awesome")+1);
+
+ assert_int_equal(flash_buffer[ALIGN_UP(sizeof(FVH), 4) + 1], 0x55);
+ assert_string_equal((const char *)buf, "is awesome");
+}
+
+int main(void)
+{
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test(efi_test_header),
+ cmocka_unit_test(efi_test_noop_existing_write),
+ cmocka_unit_test(efi_test_new_write)
+ };
+
+ return cb_run_group_tests(tests, NULL, NULL);
+}