| // 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) |
| continue; |
| |
| 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; |
| } |
| |
| 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"); |