[PATCH RFC 2/5] nvmem: Add UEFI variable NVMEM provider
Bartosz Golaszewski
brgl at kernel.org
Thu Oct 15 06:22:12 PDT 2026
On Wed, 14 Oct 2026 15:02:40 +0200, Abel Vesa <abel.vesa at oss.qualcomm.com> said:
> Add a provider that reads a UEFI variable via the EFI runtime
> GetVariable service and publishes its bytes as a read-only NVMEM device.
> NVMEM cells are described as device-tree child nodes and can be
> referenced by consumers through nvmem-cells / nvmem-cell-names.
>
> The vendor GUID is optional: when it is not provided, the variable is
> located by name by enumerating the variable store. This keeps working
> on platforms where GetVariable is emulated by firmware (e.g. the
> Qualcomm uefisecapp backend) and where direct flash access is
> unavailable to the non-secure world, in particular when Linux boots at
> EL2.
>
> If the variable has not been provisioned yet, the device is still
> registered, sized to cover the described cells and filled with zeroes,
> the same way an erased EEPROM would read. This keeps consumers probing
> and falling back to their default addresses.
>
> Assisted-by: LLM
> Signed-off-by: Abel Vesa <abel.vesa at oss.qualcomm.com>
> ---
> drivers/nvmem/Kconfig | 12 +++
> drivers/nvmem/Makefile | 2 +
> drivers/nvmem/efivar.c | 273 +++++++++++++++++++++++++++++++++++++++++++++++++
> 3 files changed, 287 insertions(+)
>
> diff --git a/drivers/nvmem/Kconfig b/drivers/nvmem/Kconfig
> index 083a5e528960..3f619dfa1cf7 100644
> --- a/drivers/nvmem/Kconfig
> +++ b/drivers/nvmem/Kconfig
> @@ -185,6 +185,18 @@ config NVMEM_BRCM_NVRAM
> This driver provides support for Broadcom's NVRAM that can be accessed
> using I/O mapping.
>
> +config NVMEM_EFIVAR
> + tristate "UEFI variable NVMEM provider"
> + depends on EFI || COMPILE_TEST
> + help
> + Say y here to expose the contents of a UEFI variable as an NVMEM
> + device. This allows the kernel to consume data provisioned in a UEFI
> + variable, referenced from consumers via the standard nvmem-cells
> + bindings.
> +
> + This driver can also be built as a module. If so, the module will
> + be called nvmem-efivar.
> +
> config NVMEM_IMX_IIM
> tristate "i.MX IC Identification Module support"
> depends on ARCH_MXC || COMPILE_TEST
> diff --git a/drivers/nvmem/Makefile b/drivers/nvmem/Makefile
> index 5e70bad9f0f9..038d3d9ee95c 100644
> --- a/drivers/nvmem/Makefile
> +++ b/drivers/nvmem/Makefile
> @@ -27,6 +27,8 @@ obj-$(CONFIG_NVMEM_BCM_OCOTP) += nvmem-bcm-ocotp.o
> nvmem-bcm-ocotp-y := bcm-ocotp.o
> obj-$(CONFIG_NVMEM_BRCM_NVRAM) += nvmem_brcm_nvram.o
> nvmem_brcm_nvram-y := brcm_nvram.o
> +obj-$(CONFIG_NVMEM_EFIVAR) += nvmem-efivar.o
> +nvmem-efivar-y := efivar.o
> obj-$(CONFIG_NVMEM_IMX_IIM) += nvmem-imx-iim.o
> nvmem-imx-iim-y := imx-iim.o
> obj-$(CONFIG_NVMEM_IMX_OCOTP) += nvmem-imx-ocotp.o
> diff --git a/drivers/nvmem/efivar.c b/drivers/nvmem/efivar.c
> new file mode 100644
> index 000000000000..699b1d67aea4
> --- /dev/null
> +++ b/drivers/nvmem/efivar.c
> @@ -0,0 +1,273 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * nvmem-efivar - expose the contents of a UEFI variable as an NVMEM device
> + *
> + * Lets the kernel consume data provisioned in a UEFI variable through the
> + * standard NVMEM cell bindings. The variable is read at probe time through
> + * the EFI runtime GetVariable service, which also works on platforms where
> + * the variable store is emulated by firmware (for example the Qualcomm
> + * uefisecapp backend) and where direct flash access is not available to the
> + * non-secure world. The variable contents are published as a read-only
> + * NVMEM device, with cells described as child nodes in the device tree and
> + * referenced by consumers through the standard "nvmem-cells" /
> + * "nvmem-cell-names" properties.
> + *
> + * If the variable has not been provisioned yet, the device is still
> + * registered, sized to cover the described cells and filled with zeroes,
> + * the same way an erased EEPROM would read. Consumers are expected to
> + * validate the data they read so such systems keep working with their
> + * defaults.
> + *
> + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
This should immediately follow the SPFX header I think.
> + */
> +
> +#include <linux/efi.h>
> +#include <linux/module.h>
> +#include <linux/mod_devicetable.h>
> +#include <linux/nvmem-provider.h>
> +#include <linux/of.h>
> +#include <linux/platform_device.h>
> +#include <linux/property.h>
> +#include <linux/slab.h>
> +#include <linux/ucs2_string.h>
> +#include <linux/uuid.h>
> +
> +struct efivar_nvmem {
> + void *data;
> + size_t size;
> +};
I would have preferred it without tabs but whatever.
> +
> +static int efivar_nvmem_reg_read(void *priv, unsigned int offset,
> + void *val, size_t bytes)
> +{
> + struct efivar_nvmem *p = priv;
> +
> + if (offset >= p->size || offset + bytes > p->size)
> + return -EINVAL;
> +
> + memcpy(val, p->data + offset, bytes);
> + return 0;
> +}
> +
> +/* Size needed to cover all cells described as child nodes. */
> +static size_t efivar_nvmem_cells_extent(struct device *dev)
> +{
> + struct device_node *child;
> + size_t extent = 0;
> + u32 reg[2];
> +
> + for_each_child_of_node(dev->of_node, child) {
You're leaking the reference to the child node. You also probably don't need
the OF variant, use device_for_each_child_node_scoped() instead.
> + if (of_property_read_u32_array(child, "reg", reg, 2))
> + continue;
> + extent = max_t(size_t, extent, (size_t)reg[0] + reg[1]);
> + }
> +
> + return extent;
> +}
> +
> +/*
> + * Find the vendor GUID of a variable given only its (UCS-2) name, by
> + * enumerating the variable store. Used when no 'efivar-guid' is provided.
> + */
> +static int efivar_nvmem_find_guid(const efi_char16_t *want, efi_guid_t *out)
> +{
> + unsigned long max = 512; /* in UCS-2 chars; variable names are short */
> + efi_char16_t *name;
> + efi_guid_t vendor = {};
> + efi_status_t status;
> + int ret = -ENODEV;
> +
> + name = kcalloc(max, sizeof(*name), GFP_KERNEL);
efi_char_16t *name __free(kfree) = kcalloc(...);
> + if (!name)
> + return -ENOMEM;
> + name[0] = 0;
Doesn't kcalloc() imply it?
> +
> + for (;;) {
> + unsigned long name_size = max * sizeof(*name);
> +
> + status = efivar_get_next_variable(&name_size, name, &vendor);
> + if (status == EFI_BUFFER_TOO_SMALL) {
> + efi_char16_t *tmp;
> +
> + max = name_size / sizeof(*name) + 1;
> + tmp = krealloc_array(name, max, sizeof(*name), GFP_KERNEL);
> + if (!tmp) {
> + ret = -ENOMEM;
> + break;
> + }
> + name = tmp;
> + continue;
> + }
> + if (status != EFI_SUCCESS) /* EFI_NOT_FOUND ends the walk */
> + break;
> +
> + if (!ucs2_strncmp(name, want, ucs2_strlen(want) + 1)) {
> + *out = vendor;
> + ret = 0;
> + break;
> + }
> + }
> +
> + kfree(name);
> + return ret;
> +}
> +
> +static int efivar_nvmem_probe(struct platform_device *pdev)
> +{
> + struct device *dev = &pdev->dev;
> + const char *name, *guid_str;
> + struct nvmem_config cfg = {};
> + struct efivar_nvmem *priv;
> + struct nvmem_device *nvmem;
> + efi_char16_t *name16;
> + efi_guid_t guid;
> + guid_t g;
> + unsigned long size;
> + efi_status_t status;
> + u32 attr;
> + int i, len, ret;
> + void *buf;
> +
> + /*
> + * The EFI variable service may not be a native EFI runtime service;
> + * backends such as the Qualcomm uefisecapp driver register the
> + * efivars operations asynchronously. Wait for them.
> + */
> + if (!efivar_is_available())
> + return -EPROBE_DEFER;
> +
> + ret = device_property_read_string(dev, "efivar-name", &name);
> + if (ret)
> + return dev_err_probe(dev, ret, "missing 'efivar-name'\n");
> +
> + /* ASCII variable name -> UCS-2 as expected by the EFI service */
> + len = strlen(name);
> + name16 = kcalloc(len + 1, sizeof(*name16), GFP_KERNEL);
efi_char_16t name16 __free(kfree) = kcalloc(...);
> + if (!name16)
> + return -ENOMEM;
> + for (i = 0; i < len; i++)
> + name16[i] = name[i];
> +
> + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> + if (!priv) {
> + kfree(name16);
> + return -ENOMEM;
> + }
> +
> + /*
> + * The vendor GUID is optional: if it is not provided, look the
> + * variable up by name across the whole store. A variable that has
> + * not been provisioned yet is reported as not found and handled
> + * below like an erased EEPROM.
> + */
> + if (!device_property_read_string(dev, "efivar-guid", &guid_str)) {
> + ret = guid_parse(guid_str, &g);
> + if (ret) {
> + kfree(name16);
> + return dev_err_probe(dev, ret, "invalid 'efivar-guid'\n");
> + }
> + memcpy(&guid, &g, sizeof(guid));
> + status = EFI_SUCCESS;
> + } else {
> + ret = efivar_nvmem_find_guid(name16, &guid);
> + if (ret && ret != -ENODEV) {
> + kfree(name16);
> + return dev_err_probe(dev, ret,
> + "failed to look up variable '%s'\n",
> + name);
> + }
> + status = ret ? EFI_NOT_FOUND : EFI_SUCCESS;
> + }
> +
> + /* First attempt with a modest buffer; grow if the firmware asks. */
> + size = 512;
> + buf = kzalloc(size, GFP_KERNEL);
void *buf __free(kfree) = kzalloc(size, GFP_KERNEL);
> + if (!buf) {
> + kfree(name16);
> + return -ENOMEM;
> + }
> +
> + if (status == EFI_SUCCESS) {
> + status = efivar_get_variable(name16, &guid, &attr, &size, buf);
> + if (status == EFI_BUFFER_TOO_SMALL) {
> + kfree(buf);
> + buf = kzalloc(size, GFP_KERNEL);
krealloc()?
> + if (!buf) {
> + kfree(name16);
> + return -ENOMEM;
> + }
> + status = efivar_get_variable(name16, &guid, &attr,
> + &size, buf);
> + }
> + }
> + kfree(name16);
> +
> + if (status == EFI_NOT_FOUND) {
> + /*
> + * Variable not provisioned yet: behave like an erased
> + * EEPROM so that consumers still probe and fall back to
> + * their defaults.
> + */
> + kfree(buf);
> + size = efivar_nvmem_cells_extent(dev);
> + if (!size)
> + return dev_err_probe(dev, -ENODEV,
> + "variable '%s' not found and no cells described\n",
> + name);
> + buf = kzalloc(size, GFP_KERNEL);
> + if (!buf)
> + return -ENOMEM;
Sprinkle in some newlines after return for better readability here and
elsewhere.
> + dev_info(dev, "variable '%s' not provisioned, reading as zeroes\n",
> + name);
> + } else if (status != EFI_SUCCESS) {
> + kfree(buf);
> + return dev_err_probe(dev, -ENODEV,
> + "GetVariable('%s') failed: 0x%lx\n",
> + name, status);
> + } else {
> + dev_dbg(dev, "variable '%s' attr=0x%x size=%lu\n",
> + name, attr, size);
> + }
> +
> + priv->data = devm_kmemdup(dev, buf, size, GFP_KERNEL);
> + kfree(buf);
> + if (!priv->data)
> + return -ENOMEM;
> + priv->size = size;
> +
> + cfg.dev = dev;
> + cfg.name = "efivar";
> + cfg.id = NVMEM_DEVID_AUTO;
> + cfg.owner = THIS_MODULE;
> + cfg.type = NVMEM_TYPE_OTP;
> + cfg.read_only = true;
> + cfg.word_size = 1;
> + cfg.stride = 1;
> + cfg.size = priv->size;
> + cfg.reg_read = efivar_nvmem_reg_read;
> + cfg.priv = priv;
> + cfg.of_node = dev->of_node;
> + cfg.add_legacy_fixed_of_cells = true;
I think it looks better without tabs. Better yet, use compound literal:
cfg = (struct nvmem_config){
.dev = dev,
...
};
> +
> + nvmem = devm_nvmem_register(dev, &cfg);
> + return PTR_ERR_OR_ZERO(nvmem);
return PTR_ERR_OR_ZERO(devm_nvmem_register(dev, &cfg)); ?
> +}
> +
> +static const struct of_device_id efivar_nvmem_of_match[] = {
> + { .compatible = "nvmem-efivar" },
> + {}
> +};
> +MODULE_DEVICE_TABLE(of, efivar_nvmem_of_match);
> +
> +static struct platform_driver efivar_nvmem_driver = {
> + .probe = efivar_nvmem_probe,
> + .driver = {
> + .name = "nvmem-efivar",
> + .of_match_table = efivar_nvmem_of_match,
> + },
> +};
> +module_platform_driver(efivar_nvmem_driver);
> +
> +MODULE_DESCRIPTION("NVMEM provider backed by a UEFI variable");
> +MODULE_IMPORT_NS("EFIVAR");
> +MODULE_LICENSE("GPL");
>
> --
> 2.54.0
>
>
Bart
More information about the ath12k
mailing list