linux/drivers/media/platform/qcom/venus/firmware.c
Mansur Alisha Shaik de15e6231e media: venus: handle use after free for iommu_map/iommu_unmap
In concurrency usecase and reboot scenario we are seeing muliple
crashes related to iommu_map/iommu_unamp of core->fw.iommu_domain.

In one case we are seeing "Unable to handle kernel NULL pointer
dereference at virtual address 0000000000000008" crash, this is
because of core->fw.iommu_domain in venus_firmware_deinit() and
trying to map in venus_boot() during venus_sys_error_handler()

Call trace:
 __iommu_map+0x4c/0x348
 iommu_map+0x5c/0x70
 venus_boot+0x184/0x230 [venus_core]
 venus_sys_error_handler+0xa0/0x14c [venus_core]
 process_one_work+0x210/0x3d0
 worker_thread+0x248/0x3f4
 kthread+0x11c/0x12c
 ret_from_fork+0x10/0x18

In second case we are seeing "Unable to handle kernel paging request
at virtual address 006b6b6b6b6b6b9b" crash, this is because of
unmapping iommu domain which is already unmapped.

Call trace:
 venus_remove+0xf8/0x108 [venus_core]
 venus_core_shutdown+0x1c/0x34 [venus_core]
 platform_drv_shutdown+0x28/0x34
 device_shutdown+0x154/0x1fc
 kernel_restart_prepare+0x40/0x4c
 kernel_restart+0x1c/0x64
 __arm64_sys_reboot+0x190/0x238
 el0_svc_common+0xa4/0x154
 el0_svc_compat_handler+0x2c/0x38
 el0_svc_compat+0x8/0x10

Signed-off-by: Mansur Alisha Shaik <mansur@codeaurora.org>
Signed-off-by: Stanimir Varbanov <stanimir.varbanov@linaro.org>
Signed-off-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
2020-11-17 06:54:59 +01:00

321 lines
6.7 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 2017 Linaro Ltd.
*/
#include <linux/device.h>
#include <linux/firmware.h>
#include <linux/kernel.h>
#include <linux/iommu.h>
#include <linux/io.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/platform_device.h>
#include <linux/of_device.h>
#include <linux/qcom_scm.h>
#include <linux/sizes.h>
#include <linux/soc/qcom/mdt_loader.h>
#include "core.h"
#include "firmware.h"
#include "hfi_venus_io.h"
#define VENUS_PAS_ID 9
#define VENUS_FW_MEM_SIZE (6 * SZ_1M)
#define VENUS_FW_START_ADDR 0x0
static void venus_reset_cpu(struct venus_core *core)
{
u32 fw_size = core->fw.mapped_mem_size;
void __iomem *base = core->base;
writel(0, base + WRAPPER_FW_START_ADDR);
writel(fw_size, base + WRAPPER_FW_END_ADDR);
writel(0, base + WRAPPER_CPA_START_ADDR);
writel(fw_size, base + WRAPPER_CPA_END_ADDR);
writel(fw_size, base + WRAPPER_NONPIX_START_ADDR);
writel(fw_size, base + WRAPPER_NONPIX_END_ADDR);
writel(0x0, base + WRAPPER_CPU_CGC_DIS);
writel(0x0, base + WRAPPER_CPU_CLOCK_CONFIG);
/* Bring ARM9 out of reset */
writel(0, base + WRAPPER_A9SS_SW_RESET);
}
int venus_set_hw_state(struct venus_core *core, bool resume)
{
int ret;
if (core->use_tz) {
ret = qcom_scm_set_remote_state(resume, 0);
if (resume && ret == -EINVAL)
ret = 0;
return ret;
}
if (resume)
venus_reset_cpu(core);
else
writel(1, core->base + WRAPPER_A9SS_SW_RESET);
return 0;
}
static int venus_load_fw(struct venus_core *core, const char *fwname,
phys_addr_t *mem_phys, size_t *mem_size)
{
const struct firmware *mdt;
struct device_node *node;
struct device *dev;
struct resource r;
ssize_t fw_size;
void *mem_va;
int ret;
*mem_phys = 0;
*mem_size = 0;
dev = core->dev;
node = of_parse_phandle(dev->of_node, "memory-region", 0);
if (!node) {
dev_err(dev, "no memory-region specified\n");
return -EINVAL;
}
ret = of_address_to_resource(node, 0, &r);
if (ret)
goto err_put_node;
ret = request_firmware(&mdt, fwname, dev);
if (ret < 0)
goto err_put_node;
fw_size = qcom_mdt_get_size(mdt);
if (fw_size < 0) {
ret = fw_size;
goto err_release_fw;
}
*mem_phys = r.start;
*mem_size = resource_size(&r);
if (*mem_size < fw_size || fw_size > VENUS_FW_MEM_SIZE) {
ret = -EINVAL;
goto err_release_fw;
}
mem_va = memremap(r.start, *mem_size, MEMREMAP_WC);
if (!mem_va) {
dev_err(dev, "unable to map memory region: %pR\n", &r);
ret = -ENOMEM;
goto err_release_fw;
}
if (core->use_tz)
ret = qcom_mdt_load(dev, mdt, fwname, VENUS_PAS_ID,
mem_va, *mem_phys, *mem_size, NULL);
else
ret = qcom_mdt_load_no_init(dev, mdt, fwname, VENUS_PAS_ID,
mem_va, *mem_phys, *mem_size, NULL);
memunmap(mem_va);
err_release_fw:
release_firmware(mdt);
err_put_node:
of_node_put(node);
return ret;
}
static int venus_boot_no_tz(struct venus_core *core, phys_addr_t mem_phys,
size_t mem_size)
{
struct iommu_domain *iommu;
struct device *dev;
int ret;
dev = core->fw.dev;
if (!dev)
return -EPROBE_DEFER;
iommu = core->fw.iommu_domain;
core->fw.mapped_mem_size = mem_size;
ret = iommu_map(iommu, VENUS_FW_START_ADDR, mem_phys, mem_size,
IOMMU_READ | IOMMU_WRITE | IOMMU_PRIV);
if (ret) {
dev_err(dev, "could not map video firmware region\n");
return ret;
}
venus_reset_cpu(core);
return 0;
}
static int venus_shutdown_no_tz(struct venus_core *core)
{
const size_t mapped = core->fw.mapped_mem_size;
struct iommu_domain *iommu;
size_t unmapped;
u32 reg;
struct device *dev = core->fw.dev;
void __iomem *base = core->base;
/* Assert the reset to ARM9 */
reg = readl_relaxed(base + WRAPPER_A9SS_SW_RESET);
reg |= WRAPPER_A9SS_SW_RESET_BIT;
writel_relaxed(reg, base + WRAPPER_A9SS_SW_RESET);
/* Make sure reset is asserted before the mapping is removed */
mb();
iommu = core->fw.iommu_domain;
if (core->fw.mapped_mem_size && iommu) {
unmapped = iommu_unmap(iommu, VENUS_FW_START_ADDR, mapped);
if (unmapped != mapped)
dev_err(dev, "failed to unmap firmware\n");
else
core->fw.mapped_mem_size = 0;
}
return 0;
}
int venus_boot(struct venus_core *core)
{
struct device *dev = core->dev;
const struct venus_resources *res = core->res;
phys_addr_t mem_phys;
size_t mem_size;
int ret;
if (!IS_ENABLED(CONFIG_QCOM_MDT_LOADER) ||
(core->use_tz && !qcom_scm_is_available()))
return -EPROBE_DEFER;
ret = venus_load_fw(core, core->res->fwname, &mem_phys, &mem_size);
if (ret) {
dev_err(dev, "fail to load video firmware\n");
return -EINVAL;
}
if (core->use_tz)
ret = qcom_scm_pas_auth_and_reset(VENUS_PAS_ID);
else
ret = venus_boot_no_tz(core, mem_phys, mem_size);
if (ret)
return ret;
if (core->use_tz && res->cp_size) {
ret = qcom_scm_mem_protect_video_var(res->cp_start,
res->cp_size,
res->cp_nonpixel_start,
res->cp_nonpixel_size);
if (ret) {
qcom_scm_pas_shutdown(VENUS_PAS_ID);
dev_err(dev, "set virtual address ranges fail (%d)\n",
ret);
return ret;
}
}
return 0;
}
int venus_shutdown(struct venus_core *core)
{
int ret;
if (core->use_tz)
ret = qcom_scm_pas_shutdown(VENUS_PAS_ID);
else
ret = venus_shutdown_no_tz(core);
return ret;
}
int venus_firmware_init(struct venus_core *core)
{
struct platform_device_info info;
struct iommu_domain *iommu_dom;
struct platform_device *pdev;
struct device_node *np;
int ret;
np = of_get_child_by_name(core->dev->of_node, "video-firmware");
if (!np) {
core->use_tz = true;
return 0;
}
memset(&info, 0, sizeof(info));
info.fwnode = &np->fwnode;
info.parent = core->dev;
info.name = np->name;
info.dma_mask = DMA_BIT_MASK(32);
pdev = platform_device_register_full(&info);
if (IS_ERR(pdev)) {
of_node_put(np);
return PTR_ERR(pdev);
}
pdev->dev.of_node = np;
ret = of_dma_configure(&pdev->dev, np, true);
if (ret) {
dev_err(core->dev, "dma configure fail\n");
goto err_unregister;
}
core->fw.dev = &pdev->dev;
iommu_dom = iommu_domain_alloc(&platform_bus_type);
if (!iommu_dom) {
dev_err(core->fw.dev, "Failed to allocate iommu domain\n");
ret = -ENOMEM;
goto err_unregister;
}
ret = iommu_attach_device(iommu_dom, core->fw.dev);
if (ret) {
dev_err(core->fw.dev, "could not attach device\n");
goto err_iommu_free;
}
core->fw.iommu_domain = iommu_dom;
of_node_put(np);
return 0;
err_iommu_free:
iommu_domain_free(iommu_dom);
err_unregister:
platform_device_unregister(pdev);
of_node_put(np);
return ret;
}
void venus_firmware_deinit(struct venus_core *core)
{
struct iommu_domain *iommu;
if (!core->fw.dev)
return;
iommu = core->fw.iommu_domain;
iommu_detach_device(iommu, core->fw.dev);
if (core->fw.iommu_domain) {
iommu_domain_free(iommu);
core->fw.iommu_domain = NULL;
}
platform_device_unregister(to_platform_device(core->fw.dev));
}