summaryrefslogtreecommitdiffstats
path: root/drivers/nvmem/layouts
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-10-02 12:17:24 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-10-02 12:17:24 -0700
commit3f1fe48a36b0b6722dc3fd421d93512bac138e9a (patch)
treeb767d7f6e26bc64334d3f14f8422d187239dc45d /drivers/nvmem/layouts
downloadlinux-stable-3f1fe48a36b0b6722dc3fd421d93512bac138e9a.tar.gz
linux-stable-3f1fe48a36b0b6722dc3fd421d93512bac138e9a.zip
Merge tag 'io_uring-7.3-20261002' of git://git.kernel.org/pub/scm/linux/kernel/git/axboe/linuxgrafted
Pull io_uring fixes from Jens Axboe: - Fix a task_work add use-after-free with SQPOLL. The sqpoll thread could pop and complete the last request while io_req_normal_work_add() was still looking at them after the mpscq push. Use the same approach as DEFER_TASKRUN to protect from that, holding an RCU read lock across the add, and have exit wait for an RCU grace period for SQPOLL rings as well. - CQE32 ring fixes: correct the free entry check for 32b CQEs, zero the big_cqe for aux CQEs, and only post the dummy skip CQE on CQE_MIXED rings - Mark the source filter table as COW when cloning bpf filters, so registering another filter on the source doesn't modify the shared table in place - Initialize the task context before running the BPF loop - Requeue zcrx multishot receives stopped by a local resource - End a TX_TIMESTAMP multishot cmd when the CQ is full (lollipopkit) * tag 'io_uring-7.3-20261002' of git://git.kernel.org/pub/scm/linux/kernel/git/axboe/linux: io_uring: fix task_work add use-after-free with SQPOLL io_uring/cmd_net: end TX_TIMESTAMP multishot when the CQ is full io_uring/zcrx: requeue multishot receives stopped by a local resource io_uring: initialize task context before running the BPF loop io_uring: zero big_cqe for aux CQEs on CQE32 rings io_uring: fix free entry check for 32b CQEs on CQE32 rings io_uring: only post the dummy skip CQE on CQE_MIXED rings io_uring/bpf_filter: mark source as COW when cloning filters
Diffstat (limited to 'drivers/nvmem/layouts')
-rw-r--r--drivers/nvmem/layouts/Kconfig42
-rw-r--r--drivers/nvmem/layouts/Makefile9
-rw-r--r--drivers/nvmem/layouts/fixed-layout.c58
-rw-r--r--drivers/nvmem/layouts/onie-tlv.c261
-rw-r--r--drivers/nvmem/layouts/sl28vpd.c169
-rw-r--r--drivers/nvmem/layouts/u-boot-env.c211
-rw-r--r--drivers/nvmem/layouts/u-boot-env.h15
7 files changed, 765 insertions, 0 deletions
diff --git a/drivers/nvmem/layouts/Kconfig b/drivers/nvmem/layouts/Kconfig
new file mode 100644
index 000000000..5e586dfeb
--- /dev/null
+++ b/drivers/nvmem/layouts/Kconfig
@@ -0,0 +1,42 @@
+# SPDX-License-Identifier: GPL-2.0
+
+config NVMEM_LAYOUTS
+ bool
+ depends on OF
+
+if NVMEM_LAYOUTS
+
+menu "Layout Types"
+
+config NVMEM_LAYOUT_SL28_VPD
+ tristate "Kontron sl28 VPD layout support"
+ select CRC8
+ help
+ Say Y here if you want to support the VPD layout of the Kontron
+ SMARC-sAL28 boards.
+
+ If unsure, say N.
+
+config NVMEM_LAYOUT_ONIE_TLV
+ tristate "ONIE tlv support"
+ select CRC32
+ help
+ Say Y here if you want to support the Open Compute Project ONIE
+ Type-Length-Value standard table.
+
+ If unsure, say N.
+
+config NVMEM_LAYOUT_U_BOOT_ENV
+ tristate "U-Boot environment variables layout"
+ select CRC32
+ select GENERIC_NET_UTILS
+ help
+ U-Boot stores its setup as environment variables. This driver adds
+ support for verifying & exporting such data. It also exposes variables
+ as NVMEM cells so they can be referenced by other drivers.
+
+ If unsure, say N.
+
+endmenu
+
+endif
diff --git a/drivers/nvmem/layouts/Makefile b/drivers/nvmem/layouts/Makefile
new file mode 100644
index 000000000..dd6c6c70b
--- /dev/null
+++ b/drivers/nvmem/layouts/Makefile
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Makefile for nvmem layouts.
+#
+
+obj-$(CONFIG_NVMEM_LAYOUTS) += fixed-layout.o
+obj-$(CONFIG_NVMEM_LAYOUT_SL28_VPD) += sl28vpd.o
+obj-$(CONFIG_NVMEM_LAYOUT_ONIE_TLV) += onie-tlv.o
+obj-$(CONFIG_NVMEM_LAYOUT_U_BOOT_ENV) += u-boot-env.o
diff --git a/drivers/nvmem/layouts/fixed-layout.c b/drivers/nvmem/layouts/fixed-layout.c
new file mode 100644
index 000000000..635d448b3
--- /dev/null
+++ b/drivers/nvmem/layouts/fixed-layout.c
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright 2026 Bootlin
+ *
+ * Authors: Mathieu Dubois-Briand <mathieu.dubois-briand@bootlin.com>
+ */
+
+#include <linux/nvmem-provider.h>
+#include <linux/of.h>
+
+#include "../internals.h"
+
+static int fixed_layout_add_cells(struct nvmem_layout *layout)
+{
+ struct device_node *np;
+ int ret;
+
+ np = of_nvmem_layout_get_container(layout->nvmem);
+ if (!np)
+ return -ENOENT;
+
+ ret = nvmem_add_cells_from_dt(layout->nvmem, np);
+ of_node_put(np);
+
+ return ret;
+}
+
+static int fixed_layout_probe(struct nvmem_layout *layout)
+{
+ layout->add_cells = fixed_layout_add_cells;
+
+ return nvmem_layout_register(layout);
+}
+
+static void fixed_layout_remove(struct nvmem_layout *layout)
+{
+ nvmem_layout_unregister(layout);
+}
+
+static const struct of_device_id fixed_layout_of_match_table[] = {
+ { .compatible = "fixed-layout", },
+ {},
+};
+
+static struct nvmem_layout_driver fixed_layout_layout = {
+ .driver = {
+ .name = "fixed-layout",
+ .of_match_table = fixed_layout_of_match_table,
+ },
+ .probe = fixed_layout_probe,
+ .remove = fixed_layout_remove,
+};
+module_nvmem_layout_driver(fixed_layout_layout);
+
+MODULE_AUTHOR("Mathieu Dubois-Briand");
+MODULE_LICENSE("GPL");
+MODULE_DEVICE_TABLE(of, fixed_layout_of_match_table);
+MODULE_DESCRIPTION("NVMEM fixed-layout driver");
diff --git a/drivers/nvmem/layouts/onie-tlv.c b/drivers/nvmem/layouts/onie-tlv.c
new file mode 100644
index 000000000..8b0f3c1b8
--- /dev/null
+++ b/drivers/nvmem/layouts/onie-tlv.c
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ONIE tlv NVMEM cells provider
+ *
+ * Copyright (C) 2022 Open Compute Group ONIE
+ * Author: Miquel Raynal <miquel.raynal@bootlin.com>
+ * Based on the nvmem driver written by: Vadym Kochan <vadym.kochan@plvision.eu>
+ * Inspired by the first layout written by: Rafał Miłecki <rafal@milecki.pl>
+ */
+
+#include <linux/crc32.h>
+#include <linux/etherdevice.h>
+#include <linux/nvmem-consumer.h>
+#include <linux/nvmem-provider.h>
+#include <linux/of.h>
+
+#define ONIE_TLV_MAX_LEN 2048
+#define ONIE_TLV_CRC_FIELD_SZ 6
+#define ONIE_TLV_CRC_SZ 4
+#define ONIE_TLV_HDR_ID "TlvInfo"
+
+struct onie_tlv_hdr {
+ u8 id[8];
+ u8 version;
+ __be16 data_len;
+} __packed;
+
+struct onie_tlv {
+ u8 type;
+ u8 len;
+} __packed;
+
+static const char *onie_tlv_cell_name(u8 type)
+{
+ switch (type) {
+ case 0x21:
+ return "product-name";
+ case 0x22:
+ return "part-number";
+ case 0x23:
+ return "serial-number";
+ case 0x24:
+ return "mac-address";
+ case 0x25:
+ return "manufacture-date";
+ case 0x26:
+ return "device-version";
+ case 0x27:
+ return "label-revision";
+ case 0x28:
+ return "platform-name";
+ case 0x29:
+ return "onie-version";
+ case 0x2A:
+ return "num-macs";
+ case 0x2B:
+ return "manufacturer";
+ case 0x2C:
+ return "country-code";
+ case 0x2D:
+ return "vendor";
+ case 0x2E:
+ return "diag-version";
+ case 0x2F:
+ return "service-tag";
+ case 0xFD:
+ return "vendor-extension";
+ case 0xFE:
+ return "crc32";
+ default:
+ break;
+ }
+
+ return NULL;
+}
+
+static int onie_tlv_mac_read_cb(void *priv, const char *id, int index,
+ unsigned int offset, void *buf,
+ size_t bytes)
+{
+ eth_addr_add(buf, index);
+
+ return 0;
+}
+
+static nvmem_cell_post_process_t onie_tlv_read_cb(u8 type, u8 *buf)
+{
+ switch (type) {
+ case 0x24:
+ return &onie_tlv_mac_read_cb;
+ default:
+ break;
+ }
+
+ return NULL;
+}
+
+static int onie_tlv_add_cells(struct device *dev, struct nvmem_device *nvmem,
+ size_t data_len, u8 *data)
+{
+ struct nvmem_cell_info cell = {};
+ struct device_node *layout;
+ struct onie_tlv tlv;
+ unsigned int hdr_len = sizeof(struct onie_tlv_hdr);
+ unsigned int offset = 0;
+ int ret;
+
+ layout = of_nvmem_layout_get_container(nvmem);
+ if (!layout)
+ return -ENOENT;
+
+ while (offset < data_len) {
+ memcpy(&tlv, data + offset, sizeof(tlv));
+ if (offset + tlv.len >= data_len) {
+ dev_err(dev, "Out of bounds field (0x%x bytes at 0x%x)\n",
+ tlv.len, hdr_len + offset);
+ break;
+ }
+
+ cell.name = onie_tlv_cell_name(tlv.type);
+ if (!cell.name)
+ goto next;
+
+ cell.offset = hdr_len + offset + sizeof(tlv.type) + sizeof(tlv.len);
+ cell.bytes = tlv.len;
+ cell.np = of_get_child_by_name(layout, cell.name);
+ cell.read_post_process = onie_tlv_read_cb(tlv.type, data + offset + sizeof(tlv));
+
+ ret = nvmem_add_one_cell(nvmem, &cell);
+ if (ret) {
+ of_node_put(layout);
+ return ret;
+ }
+
+next:
+ offset += sizeof(tlv) + tlv.len;
+ }
+
+ of_node_put(layout);
+
+ return 0;
+}
+
+static bool onie_tlv_hdr_is_valid(struct device *dev, struct onie_tlv_hdr *hdr)
+{
+ if (memcmp(hdr->id, ONIE_TLV_HDR_ID, sizeof(hdr->id))) {
+ dev_err(dev, "Invalid header\n");
+ return false;
+ }
+
+ if (hdr->version != 0x1) {
+ dev_err(dev, "Invalid version number\n");
+ return false;
+ }
+
+ return true;
+}
+
+static bool onie_tlv_crc_is_valid(struct device *dev, size_t table_len, u8 *table)
+{
+ struct onie_tlv crc_hdr;
+ u32 read_crc, calc_crc;
+ __be32 crc_be;
+
+ memcpy(&crc_hdr, table + table_len - ONIE_TLV_CRC_FIELD_SZ, sizeof(crc_hdr));
+ if (crc_hdr.type != 0xfe || crc_hdr.len != ONIE_TLV_CRC_SZ) {
+ dev_err(dev, "Invalid CRC field\n");
+ return false;
+ }
+
+ /* The table contains a JAMCRC, which is XOR'ed compared to the original
+ * CRC32 implementation as known in the Ethernet world.
+ */
+ memcpy(&crc_be, table + table_len - ONIE_TLV_CRC_SZ, ONIE_TLV_CRC_SZ);
+ read_crc = be32_to_cpu(crc_be);
+ calc_crc = crc32(~0, table, table_len - ONIE_TLV_CRC_SZ) ^ 0xFFFFFFFF;
+ if (read_crc != calc_crc) {
+ dev_err(dev, "Invalid CRC read: 0x%08x, expected: 0x%08x\n",
+ read_crc, calc_crc);
+ return false;
+ }
+
+ return true;
+}
+
+static int onie_tlv_parse_table(struct nvmem_layout *layout)
+{
+ struct nvmem_device *nvmem = layout->nvmem;
+ struct device *dev = &layout->dev;
+ struct onie_tlv_hdr hdr;
+ size_t table_len, data_len, hdr_len;
+ u8 *table, *data;
+ int ret;
+
+ ret = nvmem_device_read(nvmem, 0, sizeof(hdr), &hdr);
+ if (ret < 0)
+ return ret;
+
+ if (!onie_tlv_hdr_is_valid(dev, &hdr)) {
+ dev_err(dev, "Invalid ONIE TLV header\n");
+ return -EINVAL;
+ }
+
+ hdr_len = sizeof(hdr.id) + sizeof(hdr.version) + sizeof(hdr.data_len);
+ data_len = be16_to_cpu(hdr.data_len);
+ table_len = hdr_len + data_len;
+ if (table_len > ONIE_TLV_MAX_LEN) {
+ dev_err(dev, "Invalid ONIE TLV data length\n");
+ return -EINVAL;
+ }
+
+ table = devm_kmalloc(dev, table_len, GFP_KERNEL);
+ if (!table)
+ return -ENOMEM;
+
+ ret = nvmem_device_read(nvmem, 0, table_len, table);
+ if (ret != table_len)
+ return ret;
+
+ if (!onie_tlv_crc_is_valid(dev, table_len, table))
+ return -EINVAL;
+
+ data = table + hdr_len;
+ ret = onie_tlv_add_cells(dev, nvmem, data_len, data);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int onie_tlv_probe(struct nvmem_layout *layout)
+{
+ layout->add_cells = onie_tlv_parse_table;
+
+ return nvmem_layout_register(layout);
+}
+
+static void onie_tlv_remove(struct nvmem_layout *layout)
+{
+ nvmem_layout_unregister(layout);
+}
+
+static const struct of_device_id onie_tlv_of_match_table[] = {
+ { .compatible = "onie,tlv-layout", },
+ {},
+};
+MODULE_DEVICE_TABLE(of, onie_tlv_of_match_table);
+
+static struct nvmem_layout_driver onie_tlv_layout = {
+ .driver = {
+ .name = "onie-tlv-layout",
+ .of_match_table = onie_tlv_of_match_table,
+ },
+ .probe = onie_tlv_probe,
+ .remove = onie_tlv_remove,
+};
+module_nvmem_layout_driver(onie_tlv_layout);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Miquel Raynal <miquel.raynal@bootlin.com>");
+MODULE_DESCRIPTION("NVMEM layout driver for Onie TLV table parsing");
diff --git a/drivers/nvmem/layouts/sl28vpd.c b/drivers/nvmem/layouts/sl28vpd.c
new file mode 100644
index 000000000..e93b020b0
--- /dev/null
+++ b/drivers/nvmem/layouts/sl28vpd.c
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <linux/crc8.h>
+#include <linux/etherdevice.h>
+#include <linux/nvmem-consumer.h>
+#include <linux/nvmem-provider.h>
+#include <linux/of.h>
+#include <uapi/linux/if_ether.h>
+
+#define SL28VPD_MAGIC 'V'
+
+struct sl28vpd_header {
+ u8 magic;
+ u8 version;
+} __packed;
+
+struct sl28vpd_v1 {
+ struct sl28vpd_header header;
+ char serial_number[15];
+ u8 base_mac_address[ETH_ALEN];
+ u8 crc8;
+} __packed;
+
+static int sl28vpd_mac_address_pp(void *priv, const char *id, int index,
+ unsigned int offset, void *buf,
+ size_t bytes)
+{
+ if (bytes != ETH_ALEN)
+ return -EINVAL;
+
+ if (index < 0)
+ return -EINVAL;
+
+ if (!is_valid_ether_addr(buf))
+ return -EINVAL;
+
+ eth_addr_add(buf, index);
+
+ return 0;
+}
+
+static const struct nvmem_cell_info sl28vpd_v1_entries[] = {
+ {
+ .name = "serial-number",
+ .offset = offsetof(struct sl28vpd_v1, serial_number),
+ .bytes = sizeof_field(struct sl28vpd_v1, serial_number),
+ },
+ {
+ .name = "base-mac-address",
+ .offset = offsetof(struct sl28vpd_v1, base_mac_address),
+ .bytes = sizeof_field(struct sl28vpd_v1, base_mac_address),
+ .read_post_process = sl28vpd_mac_address_pp,
+ },
+};
+
+static int sl28vpd_v1_check_crc(struct device *dev, struct nvmem_device *nvmem)
+{
+ struct sl28vpd_v1 data_v1;
+ u8 table[CRC8_TABLE_SIZE];
+ int ret;
+ u8 crc;
+
+ crc8_populate_msb(table, 0x07);
+
+ ret = nvmem_device_read(nvmem, 0, sizeof(data_v1), &data_v1);
+ if (ret < 0)
+ return ret;
+ else if (ret != sizeof(data_v1))
+ return -EIO;
+
+ crc = crc8(table, (void *)&data_v1, sizeof(data_v1) - 1, 0);
+
+ if (crc != data_v1.crc8) {
+ dev_err(dev,
+ "Checksum is invalid (got %02x, expected %02x).\n",
+ crc, data_v1.crc8);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int sl28vpd_add_cells(struct nvmem_layout *layout)
+{
+ struct nvmem_device *nvmem = layout->nvmem;
+ struct device *dev = &layout->dev;
+ const struct nvmem_cell_info *pinfo;
+ struct nvmem_cell_info info = {0};
+ struct device_node *layout_np;
+ struct sl28vpd_header hdr;
+ int ret, i;
+
+ /* check header */
+ ret = nvmem_device_read(nvmem, 0, sizeof(hdr), &hdr);
+ if (ret < 0)
+ return ret;
+ else if (ret != sizeof(hdr))
+ return -EIO;
+
+ if (hdr.magic != SL28VPD_MAGIC) {
+ dev_err(dev, "Invalid magic value (%02x)\n", hdr.magic);
+ return -EINVAL;
+ }
+
+ if (hdr.version != 1) {
+ dev_err(dev, "Version %d is unsupported.\n", hdr.version);
+ return -EINVAL;
+ }
+
+ ret = sl28vpd_v1_check_crc(dev, nvmem);
+ if (ret)
+ return ret;
+
+ layout_np = of_nvmem_layout_get_container(nvmem);
+ if (!layout_np)
+ return -ENOENT;
+
+ for (i = 0; i < ARRAY_SIZE(sl28vpd_v1_entries); i++) {
+ pinfo = &sl28vpd_v1_entries[i];
+
+ info.name = pinfo->name;
+ info.offset = pinfo->offset;
+ info.bytes = pinfo->bytes;
+ info.read_post_process = pinfo->read_post_process;
+ info.np = of_get_child_by_name(layout_np, pinfo->name);
+
+ ret = nvmem_add_one_cell(nvmem, &info);
+ if (ret) {
+ of_node_put(layout_np);
+ return ret;
+ }
+ }
+
+ of_node_put(layout_np);
+
+ return 0;
+}
+
+static int sl28vpd_probe(struct nvmem_layout *layout)
+{
+ layout->add_cells = sl28vpd_add_cells;
+
+ return nvmem_layout_register(layout);
+}
+
+static void sl28vpd_remove(struct nvmem_layout *layout)
+{
+ nvmem_layout_unregister(layout);
+}
+
+static const struct of_device_id sl28vpd_of_match_table[] = {
+ { .compatible = "kontron,sl28-vpd" },
+ {},
+};
+MODULE_DEVICE_TABLE(of, sl28vpd_of_match_table);
+
+static struct nvmem_layout_driver sl28vpd_layout = {
+ .driver = {
+ .name = "kontron-sl28vpd-layout",
+ .of_match_table = sl28vpd_of_match_table,
+ },
+ .probe = sl28vpd_probe,
+ .remove = sl28vpd_remove,
+};
+module_nvmem_layout_driver(sl28vpd_layout);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Michael Walle <michael@walle.cc>");
+MODULE_DESCRIPTION("NVMEM layout driver for the VPD of Kontron sl28 boards");
diff --git a/drivers/nvmem/layouts/u-boot-env.c b/drivers/nvmem/layouts/u-boot-env.c
new file mode 100644
index 000000000..33ec23503
--- /dev/null
+++ b/drivers/nvmem/layouts/u-boot-env.c
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2022 - 2023 Rafał Miłecki <rafal@milecki.pl>
+ */
+
+#include <linux/crc32.h>
+#include <linux/etherdevice.h>
+#include <linux/export.h>
+#include <linux/hex.h>
+#include <linux/if_ether.h>
+#include <linux/nvmem-consumer.h>
+#include <linux/nvmem-provider.h>
+#include <linux/of.h>
+#include <linux/slab.h>
+
+#include "u-boot-env.h"
+
+struct u_boot_env_image_single {
+ __le32 crc32;
+ uint8_t data[];
+} __packed;
+
+struct u_boot_env_image_redundant {
+ __le32 crc32;
+ u8 mark;
+ uint8_t data[];
+} __packed;
+
+struct u_boot_env_image_broadcom {
+ __le32 magic;
+ __le32 len;
+ __le32 crc32;
+ DECLARE_FLEX_ARRAY(uint8_t, data);
+} __packed;
+
+static int u_boot_env_read_post_process_ethaddr(void *context, const char *id, int index,
+ unsigned int offset, void *buf, size_t bytes)
+{
+ u8 mac[ETH_ALEN];
+
+ if (!mac_pton(buf, mac))
+ return -EINVAL;
+
+ if (index)
+ eth_addr_add(mac, index);
+
+ ether_addr_copy(buf, mac);
+
+ return 0;
+}
+
+static int u_boot_env_parse_cells(struct device *dev, struct nvmem_device *nvmem, uint8_t *buf,
+ size_t data_offset, size_t data_len)
+{
+ char *data = buf + data_offset;
+ char *var, *value, *eq;
+
+ for (var = data;
+ var < data + data_len && *var;
+ var = value + strlen(value) + 1) {
+ struct nvmem_cell_info info = {};
+
+ eq = strchr(var, '=');
+ if (!eq)
+ break;
+ *eq = '\0';
+ value = eq + 1;
+
+ info.name = devm_kstrdup(dev, var, GFP_KERNEL);
+ if (!info.name)
+ return -ENOMEM;
+ info.offset = data_offset + value - data;
+ info.bytes = strlen(value);
+ info.np = of_get_child_by_name(dev->of_node, info.name);
+ if (!strcmp(var, "ethaddr") && info.bytes == MAC_ADDR_STR_LEN) {
+ info.raw_len = strlen(value);
+ info.bytes = ETH_ALEN;
+ info.read_post_process = u_boot_env_read_post_process_ethaddr;
+ }
+
+ nvmem_add_one_cell(nvmem, &info);
+ }
+
+ return 0;
+}
+
+int u_boot_env_parse(struct device *dev, struct nvmem_device *nvmem,
+ enum u_boot_env_format format)
+{
+ size_t crc32_data_offset;
+ size_t crc32_data_len;
+ size_t crc32_offset;
+ uint32_t *crc32_addr;
+ size_t data_offset;
+ size_t data_len;
+ size_t dev_size;
+ uint32_t crc32;
+ uint32_t calc;
+ uint8_t *buf;
+ u32 env_size;
+ int bytes;
+ int err;
+
+ dev_size = device_property_read_u32(dev, "env-size", &env_size) ?
+ nvmem_dev_size(nvmem) : (size_t)env_size;
+
+ buf = kzalloc(dev_size, GFP_KERNEL);
+ if (!buf) {
+ err = -ENOMEM;
+ goto err_out;
+ }
+
+ bytes = nvmem_device_read(nvmem, 0, dev_size, buf);
+ if (bytes < 0) {
+ err = bytes;
+ goto err_kfree;
+ } else if (bytes != dev_size) {
+ err = -EIO;
+ goto err_kfree;
+ }
+
+ switch (format) {
+ case U_BOOT_FORMAT_SINGLE:
+ crc32_offset = offsetof(struct u_boot_env_image_single, crc32);
+ crc32_data_offset = offsetof(struct u_boot_env_image_single, data);
+ data_offset = offsetof(struct u_boot_env_image_single, data);
+ break;
+ case U_BOOT_FORMAT_REDUNDANT:
+ crc32_offset = offsetof(struct u_boot_env_image_redundant, crc32);
+ crc32_data_offset = offsetof(struct u_boot_env_image_redundant, data);
+ data_offset = offsetof(struct u_boot_env_image_redundant, data);
+ break;
+ case U_BOOT_FORMAT_BROADCOM:
+ crc32_offset = offsetof(struct u_boot_env_image_broadcom, crc32);
+ crc32_data_offset = offsetof(struct u_boot_env_image_broadcom, data);
+ data_offset = offsetof(struct u_boot_env_image_broadcom, data);
+ break;
+ }
+
+ if (dev_size < data_offset) {
+ dev_err(dev, "Device too small for u-boot-env\n");
+ err = -EIO;
+ goto err_kfree;
+ }
+
+ crc32_addr = (uint32_t *)(buf + crc32_offset);
+ crc32 = *crc32_addr;
+ crc32_data_len = dev_size - crc32_data_offset;
+ data_len = dev_size - data_offset;
+
+ calc = crc32_le(~0, buf + crc32_data_offset, crc32_data_len) ^ ~0L;
+ if (calc != crc32) {
+ dev_err(dev, "Invalid calculated CRC32: 0x%08x (expected: 0x%08x)\n", calc, crc32);
+ err = -EINVAL;
+ goto err_kfree;
+ }
+
+ buf[dev_size - 1] = '\0';
+ err = u_boot_env_parse_cells(dev, nvmem, buf, data_offset, data_len);
+
+err_kfree:
+ kfree(buf);
+err_out:
+ return err;
+}
+EXPORT_SYMBOL_GPL(u_boot_env_parse);
+
+static int u_boot_env_add_cells(struct nvmem_layout *layout)
+{
+ struct device *dev = &layout->dev;
+ enum u_boot_env_format format;
+
+ format = (uintptr_t)device_get_match_data(dev);
+
+ return u_boot_env_parse(dev, layout->nvmem, format);
+}
+
+static int u_boot_env_probe(struct nvmem_layout *layout)
+{
+ layout->add_cells = u_boot_env_add_cells;
+
+ return nvmem_layout_register(layout);
+}
+
+static void u_boot_env_remove(struct nvmem_layout *layout)
+{
+ nvmem_layout_unregister(layout);
+}
+
+static const struct of_device_id u_boot_env_of_match_table[] = {
+ { .compatible = "u-boot,env", .data = (void *)U_BOOT_FORMAT_SINGLE, },
+ { .compatible = "u-boot,env-redundant-bool", .data = (void *)U_BOOT_FORMAT_REDUNDANT, },
+ { .compatible = "u-boot,env-redundant-count", .data = (void *)U_BOOT_FORMAT_REDUNDANT, },
+ { .compatible = "brcm,env", .data = (void *)U_BOOT_FORMAT_BROADCOM, },
+ {},
+};
+
+static struct nvmem_layout_driver u_boot_env_layout = {
+ .driver = {
+ .name = "u-boot-env-layout",
+ .of_match_table = u_boot_env_of_match_table,
+ },
+ .probe = u_boot_env_probe,
+ .remove = u_boot_env_remove,
+};
+module_nvmem_layout_driver(u_boot_env_layout);
+
+MODULE_AUTHOR("Rafał Miłecki");
+MODULE_LICENSE("GPL");
+MODULE_DEVICE_TABLE(of, u_boot_env_of_match_table);
+MODULE_DESCRIPTION("NVMEM layout driver for U-Boot environment variables");
diff --git a/drivers/nvmem/layouts/u-boot-env.h b/drivers/nvmem/layouts/u-boot-env.h
new file mode 100644
index 000000000..dd5f280ac
--- /dev/null
+++ b/drivers/nvmem/layouts/u-boot-env.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef _LINUX_NVMEM_LAYOUTS_U_BOOT_ENV_H
+#define _LINUX_NVMEM_LAYOUTS_U_BOOT_ENV_H
+
+enum u_boot_env_format {
+ U_BOOT_FORMAT_SINGLE,
+ U_BOOT_FORMAT_REDUNDANT,
+ U_BOOT_FORMAT_BROADCOM,
+};
+
+int u_boot_env_parse(struct device *dev, struct nvmem_device *nvmem,
+ enum u_boot_env_format format);
+
+#endif /* ifndef _LINUX_NVMEM_LAYOUTS_U_BOOT_ENV_H */