| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2013--2024 Intel Corporation |
| */ |
| |
| #include <linux/bitfield.h> |
| #include <linux/bits.h> |
| #include <linux/err.h> |
| #include <linux/dma-mapping.h> |
| #include <linux/gfp_types.h> |
| #include <linux/math64.h> |
| #include <linux/sizes.h> |
| #include <linux/types.h> |
| |
| #include "ipu6.h" |
| #include "ipu6-bus.h" |
| #include "ipu6-cpd.h" |
| |
| /* 15 entries + header*/ |
| #define MAX_PKG_DIR_ENT_CNT 16 |
| /* 2 qword per entry/header */ |
| #define PKG_DIR_ENT_LEN 2 |
| /* PKG_DIR size in bytes */ |
| #define PKG_DIR_SIZE ((MAX_PKG_DIR_ENT_CNT) * \ |
| (PKG_DIR_ENT_LEN) * sizeof(u64)) |
| /* _IUPKDR_ */ |
| #define PKG_DIR_HDR_MARK 0x5f4955504b44525fULL |
| |
| /* $CPD */ |
| #define CPD_HDR_MARK 0x44504324 |
| |
| #define MAX_MANIFEST_SIZE (SZ_2K * sizeof(u32)) |
| #define MAX_METADATA_SIZE SZ_64K |
| |
| #define MAX_COMPONENT_ID 127 |
| #define MAX_COMPONENT_VERSION 0xffff |
| |
| #define MANIFEST_IDX 0 |
| #define METADATA_IDX 1 |
| #define MODULEDATA_IDX 2 |
| /* |
| * PKG_DIR Entry (type == id) |
| * 63:56 55 54:48 47:32 31:24 23:0 |
| * Rsvd Rsvd Type Version Rsvd Size |
| */ |
| #define PKG_DIR_SIZE_MASK GENMASK(23, 0) |
| #define PKG_DIR_VERSION_MASK GENMASK(47, 32) |
| #define PKG_DIR_TYPE_MASK GENMASK(54, 48) |
| |
| static inline const struct ipu6_cpd_ent *ipu6_cpd_get_entry(const void *cpd, |
| u8 idx) |
| { |
| const struct ipu6_cpd_hdr *cpd_hdr = cpd; |
| const struct ipu6_cpd_ent *ent; |
| |
| ent = (const struct ipu6_cpd_ent *)((const u8 *)cpd + cpd_hdr->hdr_len); |
| return ent + idx; |
| } |
| |
| #define ipu6_cpd_get_manifest(cpd) ipu6_cpd_get_entry(cpd, MANIFEST_IDX) |
| #define ipu6_cpd_get_metadata(cpd) ipu6_cpd_get_entry(cpd, METADATA_IDX) |
| #define ipu6_cpd_get_moduledata(cpd) ipu6_cpd_get_entry(cpd, MODULEDATA_IDX) |
| |
| static const struct ipu6_cpd_metadata_cmpnt_hdr * |
| ipu6_cpd_metadata_get_cmpnt(struct ipu6_device *isp, const void *metadata, |
| unsigned int metadata_size, u8 idx) |
| { |
| size_t extn_size = sizeof(struct ipu6_cpd_metadata_extn); |
| size_t cmpnt_count = metadata_size - extn_size; |
| |
| cmpnt_count = div_u64(cmpnt_count, isp->cpd_metadata_cmpnt_size); |
| |
| if (idx > MAX_COMPONENT_ID || idx >= cmpnt_count) { |
| dev_err(&isp->pdev->dev, "Component index out of range (%d)\n", |
| idx); |
| return ERR_PTR(-EINVAL); |
| } |
| |
| return metadata + extn_size + idx * isp->cpd_metadata_cmpnt_size; |
| } |
| |
| static u32 ipu6_cpd_metadata_cmpnt_version(struct ipu6_device *isp, |
| const void *metadata, |
| unsigned int metadata_size, u8 idx) |
| { |
| const struct ipu6_cpd_metadata_cmpnt_hdr *cmpnt; |
| |
| cmpnt = ipu6_cpd_metadata_get_cmpnt(isp, metadata, metadata_size, idx); |
| if (IS_ERR(cmpnt)) |
| return PTR_ERR(cmpnt); |
| |
| return cmpnt->ver; |
| } |
| |
| static int ipu6_cpd_metadata_get_cmpnt_id(struct ipu6_device *isp, |
| const void *metadata, |
| unsigned int metadata_size, u8 idx) |
| { |
| const struct ipu6_cpd_metadata_cmpnt_hdr *cmpnt; |
| |
| cmpnt = ipu6_cpd_metadata_get_cmpnt(isp, metadata, metadata_size, idx); |
| if (IS_ERR(cmpnt)) |
| return PTR_ERR(cmpnt); |
| |
| return cmpnt->id; |
| } |
| |
| static int ipu6_cpd_parse_module_data(struct ipu6_device *isp, |
| const void *module_data, |
| unsigned int module_data_size, |
| dma_addr_t dma_addr_module_data, |
| u64 *pkg_dir, const void *metadata, |
| unsigned int metadata_size) |
| { |
| const struct ipu6_cpd_module_data_hdr *module_data_hdr; |
| const struct ipu6_cpd_hdr *dir_hdr; |
| const struct ipu6_cpd_ent *dir_ent; |
| unsigned int i; |
| u8 len; |
| |
| if (!module_data) |
| return -EINVAL; |
| |
| module_data_hdr = module_data; |
| dir_hdr = module_data + module_data_hdr->hdr_len; |
| len = dir_hdr->hdr_len; |
| dir_ent = (const struct ipu6_cpd_ent *)(((u8 *)dir_hdr) + len); |
| |
| pkg_dir[0] = PKG_DIR_HDR_MARK; |
| /* pkg_dir entry count = component count + pkg_dir header */ |
| pkg_dir[1] = dir_hdr->ent_cnt + 1; |
| |
| for (i = 0; i < dir_hdr->ent_cnt; i++, dir_ent++) { |
| u64 *p = &pkg_dir[PKG_DIR_ENT_LEN * (1 + i)]; |
| int ver, id; |
| |
| *p++ = dma_addr_module_data + dir_ent->offset; |
| id = ipu6_cpd_metadata_get_cmpnt_id(isp, metadata, |
| metadata_size, i); |
| if (id < 0 || id > MAX_COMPONENT_ID) { |
| dev_err(&isp->pdev->dev, "Invalid CPD component id\n"); |
| return -EINVAL; |
| } |
| |
| ver = ipu6_cpd_metadata_cmpnt_version(isp, metadata, |
| metadata_size, i); |
| if (ver < 0 || ver > MAX_COMPONENT_VERSION) { |
| dev_err(&isp->pdev->dev, |
| "Invalid CPD component version\n"); |
| return -EINVAL; |
| } |
| |
| *p = FIELD_PREP(PKG_DIR_SIZE_MASK, dir_ent->len) | |
| FIELD_PREP(PKG_DIR_TYPE_MASK, id) | |
| FIELD_PREP(PKG_DIR_VERSION_MASK, ver); |
| } |
| |
| return 0; |
| } |
| |
| int ipu6_cpd_create_pkg_dir(struct ipu6_bus_device *adev, const void *src) |
| { |
| dma_addr_t dma_addr_src = sg_dma_address(adev->fw_sgt.sgl); |
| const struct ipu6_cpd_ent *ent, *man_ent, *met_ent; |
| struct device *dev = &adev->auxdev.dev; |
| struct ipu6_device *isp = adev->isp; |
| unsigned int man_sz, met_sz; |
| void *pkg_dir_pos; |
| int ret; |
| |
| man_ent = ipu6_cpd_get_manifest(src); |
| man_sz = man_ent->len; |
| |
| met_ent = ipu6_cpd_get_metadata(src); |
| met_sz = met_ent->len; |
| |
| adev->pkg_dir_size = PKG_DIR_SIZE + man_sz + met_sz; |
| adev->pkg_dir = dma_alloc_attrs(dev, adev->pkg_dir_size, |
| &adev->pkg_dir_dma_addr, GFP_KERNEL, 0); |
| if (!adev->pkg_dir) |
| return -ENOMEM; |
| |
| /* |
| * pkg_dir entry/header: |
| * qword | 63:56 | 55 | 54:48 | 47:32 | 31:24 | 23:0 |
| * N Address/Offset/"_IUPKDR_" |
| * N + 1 | rsvd | rsvd | type | ver | rsvd | size |
| * |
| * We can ignore other fields that size in N + 1 qword as they |
| * are 0 anyway. Just setting size for now. |
| */ |
| |
| ent = ipu6_cpd_get_moduledata(src); |
| |
| ret = ipu6_cpd_parse_module_data(isp, src + ent->offset, |
| ent->len, dma_addr_src + ent->offset, |
| adev->pkg_dir, src + met_ent->offset, |
| met_ent->len); |
| if (ret) { |
| dev_err(&isp->pdev->dev, "Failed to parse module data\n"); |
| dma_free_attrs(dev, adev->pkg_dir_size, |
| adev->pkg_dir, adev->pkg_dir_dma_addr, 0); |
| return ret; |
| } |
| |
| /* Copy manifest after pkg_dir */ |
| pkg_dir_pos = adev->pkg_dir + PKG_DIR_ENT_LEN * MAX_PKG_DIR_ENT_CNT; |
| memcpy(pkg_dir_pos, src + man_ent->offset, man_sz); |
| |
| /* Copy metadata after manifest */ |
| pkg_dir_pos += man_sz; |
| memcpy(pkg_dir_pos, src + met_ent->offset, met_sz); |
| |
| dma_sync_single_range_for_device(dev, adev->pkg_dir_dma_addr, |
| 0, adev->pkg_dir_size, DMA_TO_DEVICE); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_NS_GPL(ipu6_cpd_create_pkg_dir, INTEL_IPU6); |
| |
| void ipu6_cpd_free_pkg_dir(struct ipu6_bus_device *adev) |
| { |
| dma_free_attrs(&adev->auxdev.dev, adev->pkg_dir_size, adev->pkg_dir, |
| adev->pkg_dir_dma_addr, 0); |
| } |
| EXPORT_SYMBOL_NS_GPL(ipu6_cpd_free_pkg_dir, INTEL_IPU6); |
| |
| static int ipu6_cpd_validate_cpd(struct ipu6_device *isp, const void *cpd, |
| unsigned long cpd_size, |
| unsigned long data_size) |
| { |
| const struct ipu6_cpd_hdr *cpd_hdr = cpd; |
| const struct ipu6_cpd_ent *ent; |
| unsigned int i; |
| u8 len; |
| |
| len = cpd_hdr->hdr_len; |
| |
| /* Ensure cpd hdr is within moduledata */ |
| if (cpd_size < len) { |
| dev_err(&isp->pdev->dev, "Invalid CPD moduledata size\n"); |
| return -EINVAL; |
| } |
| |
| /* Sanity check for CPD header */ |
| if ((cpd_size - len) / sizeof(*ent) < cpd_hdr->ent_cnt) { |
| dev_err(&isp->pdev->dev, "Invalid CPD header\n"); |
| return -EINVAL; |
| } |
| |
| /* Ensure that all entries are within moduledata */ |
| ent = (const struct ipu6_cpd_ent *)(((const u8 *)cpd_hdr) + len); |
| for (i = 0; i < cpd_hdr->ent_cnt; i++, ent++) { |
| if (data_size < ent->offset || |
| data_size - ent->offset < ent->len) { |
| dev_err(&isp->pdev->dev, "Invalid CPD entry (%d)\n", i); |
| return -EINVAL; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int ipu6_cpd_validate_moduledata(struct ipu6_device *isp, |
| const void *moduledata, |
| u32 moduledata_size) |
| { |
| const struct ipu6_cpd_module_data_hdr *mod_hdr = moduledata; |
| int ret; |
| |
| /* Ensure moduledata hdr is within moduledata */ |
| if (moduledata_size < sizeof(*mod_hdr) || |
| moduledata_size < mod_hdr->hdr_len) { |
| dev_err(&isp->pdev->dev, "Invalid CPD moduledata size\n"); |
| return -EINVAL; |
| } |
| |
| dev_info(&isp->pdev->dev, "FW version: %x\n", mod_hdr->fw_pkg_date); |
| ret = ipu6_cpd_validate_cpd(isp, moduledata + mod_hdr->hdr_len, |
| moduledata_size - mod_hdr->hdr_len, |
| moduledata_size); |
| if (ret) { |
| dev_err(&isp->pdev->dev, "Invalid CPD in moduledata\n"); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int ipu6_cpd_validate_metadata(struct ipu6_device *isp, |
| const void *metadata, u32 meta_size) |
| { |
| const struct ipu6_cpd_metadata_extn *extn = metadata; |
| |
| /* Sanity check for metadata size */ |
| if (meta_size < sizeof(*extn) || meta_size > MAX_METADATA_SIZE) { |
| dev_err(&isp->pdev->dev, "Invalid CPD metadata\n"); |
| return -EINVAL; |
| } |
| |
| /* Validate extension and image types */ |
| if (extn->extn_type != IPU6_CPD_METADATA_EXTN_TYPE_IUNIT || |
| extn->img_type != IPU6_CPD_METADATA_IMAGE_TYPE_MAIN_FIRMWARE) { |
| dev_err(&isp->pdev->dev, |
| "Invalid CPD metadata descriptor img_type (%d)\n", |
| extn->img_type); |
| return -EINVAL; |
| } |
| |
| /* Validate metadata size multiple of metadata components */ |
| if ((meta_size - sizeof(*extn)) % isp->cpd_metadata_cmpnt_size) { |
| dev_err(&isp->pdev->dev, "Invalid CPD metadata size\n"); |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| int ipu6_cpd_validate_cpd_file(struct ipu6_device *isp, const void *cpd_file, |
| unsigned long cpd_file_size) |
| { |
| const struct ipu6_cpd_hdr *hdr = cpd_file; |
| const struct ipu6_cpd_ent *ent; |
| int ret; |
| |
| ret = ipu6_cpd_validate_cpd(isp, cpd_file, cpd_file_size, |
| cpd_file_size); |
| if (ret) { |
| dev_err(&isp->pdev->dev, "Invalid CPD in file\n"); |
| return ret; |
| } |
| |
| /* Check for CPD file marker */ |
| if (hdr->hdr_mark != CPD_HDR_MARK) { |
| dev_err(&isp->pdev->dev, "Invalid CPD header\n"); |
| return -EINVAL; |
| } |
| |
| /* Sanity check for manifest size */ |
| ent = ipu6_cpd_get_manifest(cpd_file); |
| if (ent->len > MAX_MANIFEST_SIZE) { |
| dev_err(&isp->pdev->dev, "Invalid CPD manifest size\n"); |
| return -EINVAL; |
| } |
| |
| /* Validate metadata */ |
| ent = ipu6_cpd_get_metadata(cpd_file); |
| ret = ipu6_cpd_validate_metadata(isp, cpd_file + ent->offset, ent->len); |
| if (ret) { |
| dev_err(&isp->pdev->dev, "Invalid CPD metadata\n"); |
| return ret; |
| } |
| |
| /* Validate moduledata */ |
| ent = ipu6_cpd_get_moduledata(cpd_file); |
| ret = ipu6_cpd_validate_moduledata(isp, cpd_file + ent->offset, |
| ent->len); |
| if (ret) |
| dev_err(&isp->pdev->dev, "Invalid CPD moduledata\n"); |
| |
| return ret; |
| } |