1101 lines
27 KiB
C
1101 lines
27 KiB
C
/* Copyright (c) 2012-2015, The Linux Foundation. All rights reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 and
|
|
* only version 2 as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
|
#include <linux/kernel.h>
|
|
#include <linux/module.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/io.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/list.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/iommu.h>
|
|
#include <linux/clk.h>
|
|
#include <linux/scatterlist.h>
|
|
#include <linux/of.h>
|
|
#include <linux/of_device.h>
|
|
#include <linux/kmemleak.h>
|
|
#include <linux/dma-mapping.h>
|
|
#include <soc/qcom/scm.h>
|
|
#include <soc/qcom/secure_buffer.h>
|
|
|
|
#include <asm/cacheflush.h>
|
|
#include <linux/sizes.h>
|
|
|
|
#include "msm_iommu_pagetable.h"
|
|
#include "msm_iommu_perfmon.h"
|
|
#include "msm_iommu_hw-v1.h"
|
|
#include "msm_iommu_priv.h"
|
|
#include <linux/qcom_iommu.h>
|
|
#include <trace/events/kmem.h>
|
|
|
|
/* bitmap of the page sizes currently supported */
|
|
#define MSM_IOMMU_PGSIZES (SZ_4K | SZ_64K | SZ_1M | SZ_16M)
|
|
|
|
/* commands for SCM_SVC_MP */
|
|
#define IOMMU_SECURE_CFG 2
|
|
#define IOMMU_SECURE_PTBL_SIZE 3
|
|
#define IOMMU_SECURE_PTBL_INIT 4
|
|
#define IOMMU_SET_CP_POOL_SIZE 5
|
|
#define IOMMU_SECURE_MAP 6
|
|
#define IOMMU_SECURE_UNMAP 7
|
|
#define IOMMU_SECURE_MAP2 0x0B
|
|
#define IOMMU_SECURE_MAP2_FLAT 0x12
|
|
#define IOMMU_SECURE_UNMAP2 0x0C
|
|
#define IOMMU_SECURE_UNMAP2_FLAT 0x13
|
|
#define IOMMU_TLBINVAL_FLAG 0x00000001
|
|
|
|
/* commands for SCM_SVC_UTIL */
|
|
#define IOMMU_DUMP_SMMU_FAULT_REGS 0X0C
|
|
#define MAXIMUM_VIRT_SIZE (300*SZ_1M)
|
|
|
|
|
|
#define MAKE_VERSION(major, minor, patch) \
|
|
(((major & 0x3FF) << 22) | ((minor & 0x3FF) << 12) | (patch & 0xFFF))
|
|
|
|
|
|
static struct iommu_access_ops *iommu_access_ops;
|
|
static int is_secure;
|
|
|
|
static const struct of_device_id msm_smmu_list[] = {
|
|
{ .compatible = "qcom,msm-smmu-v1", },
|
|
{ .compatible = "qcom,msm-smmu-v2", },
|
|
{ }
|
|
};
|
|
|
|
struct msm_scm_paddr_list {
|
|
unsigned int list;
|
|
unsigned int list_size;
|
|
unsigned int size;
|
|
};
|
|
|
|
struct msm_scm_mapping_info {
|
|
unsigned int id;
|
|
unsigned int ctx_id;
|
|
unsigned int va;
|
|
unsigned int size;
|
|
};
|
|
|
|
struct msm_scm_map2_req {
|
|
struct msm_scm_paddr_list plist;
|
|
struct msm_scm_mapping_info info;
|
|
unsigned int flags;
|
|
};
|
|
|
|
struct msm_scm_unmap2_req {
|
|
struct msm_scm_mapping_info info;
|
|
unsigned int flags;
|
|
};
|
|
|
|
struct msm_cp_pool_size {
|
|
uint32_t size;
|
|
uint32_t spare;
|
|
};
|
|
|
|
#define NUM_DUMP_REGS 14
|
|
/*
|
|
* some space to allow the number of registers returned by the secure
|
|
* environment to grow
|
|
*/
|
|
#define WIGGLE_ROOM (NUM_DUMP_REGS * 2)
|
|
/* Each entry is a (reg_addr, reg_val) pair, hence the * 2 */
|
|
#define SEC_DUMP_SIZE ((NUM_DUMP_REGS * 2) + WIGGLE_ROOM)
|
|
|
|
struct msm_scm_fault_regs_dump {
|
|
uint32_t dump_size;
|
|
uint32_t dump_data[SEC_DUMP_SIZE];
|
|
} __aligned(PAGE_SIZE);
|
|
|
|
void msm_iommu_sec_set_access_ops(struct iommu_access_ops *access_ops)
|
|
{
|
|
iommu_access_ops = access_ops;
|
|
}
|
|
|
|
static int msm_iommu_dump_fault_regs(int smmu_id, int cb_num,
|
|
struct msm_scm_fault_regs_dump *regs)
|
|
{
|
|
int ret;
|
|
struct scm_desc desc = {0};
|
|
|
|
struct msm_scm_fault_regs_dump_req {
|
|
uint32_t id;
|
|
uint32_t cb_num;
|
|
uint32_t buff;
|
|
uint32_t len;
|
|
} req_info;
|
|
int resp = 0;
|
|
|
|
desc.args[0] = req_info.id = smmu_id;
|
|
desc.args[1] = req_info.cb_num = cb_num;
|
|
/* virt_to_phys(regs) may be greater than 4GB */
|
|
req_info.buff = virt_to_phys(regs);
|
|
desc.args[2] = virt_to_phys(regs);
|
|
desc.args[3] = req_info.len = sizeof(*regs);
|
|
desc.arginfo = SCM_ARGS(4, SCM_VAL, SCM_VAL, SCM_RW, SCM_VAL);
|
|
|
|
dmac_clean_range(regs, regs + 1);
|
|
if (!is_scm_armv8())
|
|
ret = scm_call(SCM_SVC_UTIL, IOMMU_DUMP_SMMU_FAULT_REGS,
|
|
&req_info, sizeof(req_info), &resp, 1);
|
|
else
|
|
ret = scm_call2(SCM_SIP_FNID(SCM_SVC_UTIL,
|
|
IOMMU_DUMP_SMMU_FAULT_REGS), &desc);
|
|
dmac_inv_range(regs, regs + 1);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int msm_iommu_reg_dump_to_regs(
|
|
struct msm_iommu_context_reg ctx_regs[],
|
|
struct msm_scm_fault_regs_dump *dump, struct msm_iommu_drvdata *drvdata,
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata)
|
|
{
|
|
int i, j, ret = 0;
|
|
const uint32_t nvals = (dump->dump_size / sizeof(uint32_t));
|
|
uint32_t *it = (uint32_t *) dump->dump_data;
|
|
const uint32_t * const end = ((uint32_t *) dump) + nvals;
|
|
phys_addr_t phys_base = drvdata->phys_base;
|
|
int ctx = ctx_drvdata->num;
|
|
|
|
if (!nvals)
|
|
return -EINVAL;
|
|
|
|
for (i = 1; it < end; it += 2, i += 2) {
|
|
unsigned int reg_offset;
|
|
uint32_t addr = *it;
|
|
uint32_t val = *(it + 1);
|
|
struct msm_iommu_context_reg *reg = NULL;
|
|
|
|
if (addr < phys_base) {
|
|
pr_err("Bogus-looking register (0x%x) for Iommu with base at %pa. Skipping.\n",
|
|
addr, &phys_base);
|
|
continue;
|
|
}
|
|
reg_offset = addr - phys_base;
|
|
|
|
for (j = 0; j < MAX_DUMP_REGS; ++j) {
|
|
struct dump_regs_tbl_entry dump_reg = dump_regs_tbl[j];
|
|
void *test_reg;
|
|
unsigned int test_offset;
|
|
|
|
switch (dump_reg.dump_reg_type) {
|
|
case DRT_CTX_REG:
|
|
test_reg = CTX_REG(dump_reg.reg_offset,
|
|
drvdata->cb_base, ctx);
|
|
break;
|
|
case DRT_GLOBAL_REG:
|
|
test_reg = GLB_REG(
|
|
dump_reg.reg_offset, drvdata->glb_base);
|
|
break;
|
|
case DRT_GLOBAL_REG_N:
|
|
test_reg = GLB_REG_N(
|
|
drvdata->glb_base, ctx,
|
|
dump_reg.reg_offset);
|
|
break;
|
|
default:
|
|
pr_err("Unknown dump_reg_type: 0x%x\n",
|
|
dump_reg.dump_reg_type);
|
|
BUG();
|
|
break;
|
|
}
|
|
test_offset = test_reg - drvdata->glb_base;
|
|
if (test_offset == reg_offset) {
|
|
reg = &ctx_regs[j];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (reg == NULL) {
|
|
pr_debug("Unknown register in secure CB dump: %x\n",
|
|
addr);
|
|
continue;
|
|
}
|
|
|
|
if (reg->valid) {
|
|
WARN(1, "Invalid (repeated?) register in CB dump: %x\n",
|
|
addr);
|
|
continue;
|
|
}
|
|
|
|
reg->val = val;
|
|
reg->valid = true;
|
|
}
|
|
|
|
if (i != nvals) {
|
|
pr_err("Invalid dump! %d != %d\n", i, nvals);
|
|
ret = 1;
|
|
}
|
|
|
|
for (i = 0; i < MAX_DUMP_REGS; ++i) {
|
|
if (!ctx_regs[i].valid) {
|
|
if (dump_regs_tbl[i].must_be_present) {
|
|
pr_err("Register missing from dump for ctx %d: %s, 0x%x\n",
|
|
ctx,
|
|
dump_regs_tbl[i].name,
|
|
dump_regs_tbl[i].reg_offset);
|
|
ret = 1;
|
|
}
|
|
ctx_regs[i].val = 0xd00dfeed;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void print_iova_to_phys(struct msm_iommu_ctx_drvdata *ctx_drvdata,
|
|
struct msm_iommu_context_reg ctx_regs[MAX_DUMP_REGS])
|
|
{
|
|
phys_addr_t pagetable_phys;
|
|
u64 faulty_iova = 0;
|
|
|
|
if (ctx_drvdata->attached_domain &&
|
|
!ctx_drvdata->secure_context) {
|
|
faulty_iova = COMBINE_DUMP_REG(
|
|
ctx_regs[DUMP_REG_FAR1].val,
|
|
ctx_regs[DUMP_REG_FAR0].val);
|
|
pagetable_phys = msm_iommu_iova_to_phys_soft(
|
|
ctx_drvdata->attached_domain,
|
|
faulty_iova);
|
|
pr_err("Page table in DDR shows PA = %lx\n",
|
|
(unsigned long) pagetable_phys);
|
|
}
|
|
}
|
|
|
|
irqreturn_t msm_iommu_secure_fault_handler_v2(int irq, void *dev_id)
|
|
{
|
|
struct platform_device *pdev = dev_id;
|
|
struct msm_iommu_drvdata *drvdata;
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata;
|
|
struct msm_scm_fault_regs_dump *regs;
|
|
int tmp, ret = IRQ_HANDLED;
|
|
|
|
iommu_access_ops->iommu_lock_acquire(0);
|
|
|
|
BUG_ON(!pdev);
|
|
|
|
drvdata = dev_get_drvdata(pdev->dev.parent);
|
|
BUG_ON(!drvdata);
|
|
|
|
ctx_drvdata = dev_get_drvdata(&pdev->dev);
|
|
BUG_ON(!ctx_drvdata);
|
|
|
|
regs = kzalloc(sizeof(*regs), GFP_KERNEL);
|
|
if (!regs)
|
|
goto lock_release;
|
|
|
|
if (!drvdata->ctx_attach_count) {
|
|
pr_err("Unexpected IOMMU page fault from secure context bank!\n");
|
|
pr_err("name = %s\n", drvdata->name);
|
|
pr_err("Power is OFF. Unable to read page fault information\n");
|
|
/*
|
|
* We cannot determine which context bank caused the issue so
|
|
* we just return handled here to ensure IRQ handler code is
|
|
* happy
|
|
*/
|
|
goto free_regs;
|
|
}
|
|
|
|
iommu_access_ops->iommu_clk_on(drvdata);
|
|
tmp = msm_iommu_dump_fault_regs(drvdata->sec_id,
|
|
ctx_drvdata->num, regs);
|
|
|
|
if (tmp) {
|
|
pr_err("%s: Couldn't dump fault registers (%d) %s, ctx: %d\n",
|
|
__func__, tmp, drvdata->name, ctx_drvdata->num);
|
|
goto clock_off;
|
|
} else {
|
|
struct msm_iommu_context_reg ctx_regs[MAX_DUMP_REGS];
|
|
|
|
memset(ctx_regs, 0, sizeof(ctx_regs));
|
|
tmp = msm_iommu_reg_dump_to_regs(
|
|
ctx_regs, regs, drvdata, ctx_drvdata);
|
|
if (tmp < 0) {
|
|
ret = IRQ_NONE;
|
|
pr_err("Incorrect response from secure environment\n");
|
|
goto clock_off;
|
|
}
|
|
|
|
if (ctx_regs[DUMP_REG_FSR].val & 0x1FF) {
|
|
if (tmp)
|
|
pr_err("Incomplete fault register dump. Printout will be incomplete.\n");
|
|
if (!ctx_drvdata->attached_domain) {
|
|
pr_err("Bad domain in interrupt handler\n");
|
|
tmp = -ENOSYS;
|
|
} else {
|
|
tmp = report_iommu_fault(
|
|
ctx_drvdata->attached_domain,
|
|
&ctx_drvdata->pdev->dev,
|
|
COMBINE_DUMP_REG(
|
|
ctx_regs[DUMP_REG_FAR1].val,
|
|
ctx_regs[DUMP_REG_FAR0].val),
|
|
0);
|
|
}
|
|
|
|
/* if the fault wasn't handled by someone else: */
|
|
if (tmp == -ENOSYS) {
|
|
pr_err("Unexpected IOMMU page fault from secure context bank!\n");
|
|
pr_err("name = %s\n", drvdata->name);
|
|
pr_err("context = %s (%d)\n", ctx_drvdata->name,
|
|
ctx_drvdata->num);
|
|
pr_err("Interesting registers:\n");
|
|
print_ctx_regs(ctx_regs);
|
|
print_iova_to_phys(ctx_drvdata, ctx_regs);
|
|
}
|
|
} else {
|
|
ret = IRQ_NONE;
|
|
}
|
|
}
|
|
clock_off:
|
|
iommu_access_ops->iommu_clk_off(drvdata);
|
|
free_regs:
|
|
kfree(regs);
|
|
lock_release:
|
|
iommu_access_ops->iommu_lock_release(0);
|
|
return ret;
|
|
}
|
|
|
|
static int msm_iommu_sec_ptbl_init(void)
|
|
{
|
|
struct device_node *np;
|
|
struct msm_scm_ptbl_init {
|
|
unsigned int paddr;
|
|
unsigned int size;
|
|
unsigned int spare;
|
|
} pinit = {0};
|
|
int psize[2] = {0, 0};
|
|
unsigned int spare;
|
|
int ret, ptbl_ret = 0;
|
|
int version;
|
|
/* Use a dummy device for dma_alloc_attrs allocation */
|
|
struct device dev = { 0 };
|
|
void *cpu_addr;
|
|
dma_addr_t paddr;
|
|
DEFINE_DMA_ATTRS(attrs);
|
|
struct scm_desc desc = {0};
|
|
|
|
for_each_matching_node(np, msm_smmu_list)
|
|
if (of_find_property(np, "qcom,iommu-secure-id", NULL) &&
|
|
of_device_is_available(np))
|
|
break;
|
|
|
|
if (!np)
|
|
return 0;
|
|
|
|
of_node_put(np);
|
|
|
|
version = scm_get_feat_version(SCM_SVC_MP);
|
|
|
|
if (version >= MAKE_VERSION(1, 1, 1)) {
|
|
struct msm_cp_pool_size psize;
|
|
int retval;
|
|
struct scm_desc desc = {0};
|
|
|
|
desc.args[0] = psize.size = MAXIMUM_VIRT_SIZE;
|
|
desc.args[1] = psize.spare = 0;
|
|
desc.arginfo = SCM_ARGS(2);
|
|
|
|
if (!is_scm_armv8())
|
|
ret = scm_call(SCM_SVC_MP, IOMMU_SET_CP_POOL_SIZE,
|
|
&psize, sizeof(psize), &retval,
|
|
sizeof(retval));
|
|
else
|
|
ret = scm_call2(SCM_SIP_FNID(SCM_SVC_MP,
|
|
IOMMU_SET_CP_POOL_SIZE), &desc);
|
|
|
|
if (ret) {
|
|
pr_err("scm call IOMMU_SET_CP_POOL_SIZE failed\n");
|
|
goto fail;
|
|
}
|
|
|
|
}
|
|
|
|
if (!is_scm_armv8()) {
|
|
ret = scm_call(SCM_SVC_MP, IOMMU_SECURE_PTBL_SIZE, &spare,
|
|
sizeof(spare), psize, sizeof(psize));
|
|
} else {
|
|
struct scm_desc desc = {0};
|
|
|
|
desc.args[0] = spare;
|
|
desc.arginfo = SCM_ARGS(1);
|
|
ret = scm_call2(SCM_SIP_FNID(SCM_SVC_MP,
|
|
IOMMU_SECURE_PTBL_SIZE), &desc);
|
|
psize[0] = desc.ret[0];
|
|
psize[1] = desc.ret[1];
|
|
}
|
|
if (ret) {
|
|
pr_err("scm call IOMMU_SECURE_PTBL_SIZE failed\n");
|
|
goto fail;
|
|
}
|
|
|
|
if (psize[1]) {
|
|
pr_err("scm call IOMMU_SECURE_PTBL_SIZE failed\n");
|
|
goto fail;
|
|
}
|
|
|
|
dma_set_attr(DMA_ATTR_NO_KERNEL_MAPPING, &attrs);
|
|
dev.coherent_dma_mask = DMA_BIT_MASK(sizeof(dma_addr_t) * 8);
|
|
cpu_addr = dma_alloc_attrs(&dev, psize[0], &paddr, GFP_KERNEL, &attrs);
|
|
if (!cpu_addr) {
|
|
pr_err("%s: Failed to allocate %d bytes for PTBL\n",
|
|
__func__, psize[0]);
|
|
ret = -ENOMEM;
|
|
goto fail;
|
|
}
|
|
|
|
pinit.paddr = (unsigned int)paddr;
|
|
/* paddr may be a physical address > 4GB */
|
|
desc.args[0] = paddr;
|
|
desc.args[1] = pinit.size = psize[0];
|
|
desc.args[2] = pinit.spare;
|
|
desc.arginfo = SCM_ARGS(3, SCM_RW, SCM_VAL, SCM_VAL);
|
|
|
|
if (!is_scm_armv8()) {
|
|
ret = scm_call(SCM_SVC_MP, IOMMU_SECURE_PTBL_INIT, &pinit,
|
|
sizeof(pinit), &ptbl_ret, sizeof(ptbl_ret));
|
|
} else {
|
|
ret = scm_call2(SCM_SIP_FNID(SCM_SVC_MP,
|
|
IOMMU_SECURE_PTBL_INIT), &desc);
|
|
ptbl_ret = desc.ret[0];
|
|
}
|
|
if (ret) {
|
|
pr_err("scm call IOMMU_SECURE_PTBL_INIT failed\n");
|
|
goto fail_mem;
|
|
}
|
|
if (ptbl_ret) {
|
|
pr_err("scm call IOMMU_SECURE_PTBL_INIT extended ret fail\n");
|
|
goto fail_mem;
|
|
}
|
|
|
|
return 0;
|
|
|
|
fail_mem:
|
|
dma_free_attrs(&dev, psize[0], cpu_addr, paddr, &attrs);
|
|
fail:
|
|
return ret;
|
|
}
|
|
|
|
int msm_iommu_sec_program_iommu(struct msm_iommu_drvdata *drvdata,
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata)
|
|
{
|
|
int ret, scm_ret = 0;
|
|
|
|
if (drvdata->smmu_local_base) {
|
|
writel_relaxed(0xFFFFFFFF, drvdata->smmu_local_base +
|
|
SMMU_INTR_SEL_NS);
|
|
/* make sure SMMU_INTR_SEL_NS is seen */
|
|
mb();
|
|
}
|
|
|
|
ret = scm_restore_sec_cfg(drvdata->sec_id, ctx_drvdata->num, &scm_ret);
|
|
if (ret || scm_ret) {
|
|
pr_err("scm call IOMMU_SECURE_CFG failed\n");
|
|
return ret ? ret : -EINVAL;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int msm_iommu_sec_map2(struct msm_scm_map2_req *map)
|
|
{
|
|
struct scm_desc desc = {0};
|
|
u32 resp;
|
|
int ret;
|
|
|
|
desc.args[0] = map->plist.list;
|
|
desc.args[1] = map->plist.list_size;
|
|
desc.args[2] = map->plist.size;
|
|
desc.args[3] = map->info.id;
|
|
desc.args[4] = map->info.ctx_id;
|
|
desc.args[5] = map->info.va;
|
|
desc.args[6] = map->info.size;
|
|
desc.args[7] = map->flags = 0;
|
|
desc.arginfo = SCM_ARGS(8, SCM_RW, SCM_VAL, SCM_VAL, SCM_VAL, SCM_VAL,
|
|
SCM_VAL, SCM_VAL, SCM_VAL);
|
|
if (!is_scm_armv8()) {
|
|
ret = scm_call(SCM_SVC_MP, IOMMU_SECURE_MAP2, map, sizeof(*map),
|
|
&resp, sizeof(resp));
|
|
} else {
|
|
ret = scm_call2(SCM_SIP_FNID(SCM_SVC_MP,
|
|
IOMMU_SECURE_MAP2_FLAT), &desc);
|
|
resp = desc.ret[0];
|
|
}
|
|
if (ret || resp)
|
|
return -EINVAL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int msm_iommu_sec_ptbl_map(struct msm_iommu_drvdata *iommu_drvdata,
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata,
|
|
unsigned long va, phys_addr_t pa, size_t len)
|
|
{
|
|
struct msm_scm_map2_req map;
|
|
void *flush_va, *flush_va_end;
|
|
int ret = 0;
|
|
|
|
if (!IS_ALIGNED(va, SZ_1M) || !IS_ALIGNED(len, SZ_1M) ||
|
|
!IS_ALIGNED(pa, SZ_1M))
|
|
return -EINVAL;
|
|
map.plist.list = virt_to_phys(&pa);
|
|
map.plist.list_size = 1;
|
|
map.plist.size = len;
|
|
map.info.id = iommu_drvdata->sec_id;
|
|
map.info.ctx_id = ctx_drvdata->num;
|
|
map.info.va = va;
|
|
map.info.size = len;
|
|
|
|
flush_va = &pa;
|
|
flush_va_end = (void *)
|
|
(((unsigned long) flush_va) + sizeof(phys_addr_t));
|
|
|
|
/*
|
|
* Ensure that the buffer is in RAM by the time it gets to TZ
|
|
*/
|
|
dmac_clean_range(flush_va, flush_va_end);
|
|
|
|
ret = msm_iommu_sec_map2(&map);
|
|
if (ret)
|
|
return -EINVAL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static unsigned int get_phys_addr(struct scatterlist *sg)
|
|
{
|
|
/*
|
|
* Try sg_dma_address first so that we can
|
|
* map carveout regions that do not have a
|
|
* struct page associated with them.
|
|
*/
|
|
unsigned int pa = sg_dma_address(sg);
|
|
|
|
if (pa == 0)
|
|
pa = sg_phys(sg);
|
|
return pa;
|
|
}
|
|
|
|
static int msm_iommu_sec_ptbl_map_range(struct msm_iommu_drvdata *iommu_drvdata,
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata,
|
|
unsigned long va, struct scatterlist *sg, size_t len)
|
|
{
|
|
struct scatterlist *sgiter;
|
|
struct msm_scm_map2_req map;
|
|
unsigned int *pa_list = 0;
|
|
unsigned int pa, cnt;
|
|
void *flush_va, *flush_va_end;
|
|
unsigned int offset = 0, chunk_offset = 0;
|
|
int ret;
|
|
|
|
if (!IS_ALIGNED(va, SZ_1M) || !IS_ALIGNED(len, SZ_1M))
|
|
return -EINVAL;
|
|
|
|
map.info.id = iommu_drvdata->sec_id;
|
|
map.info.ctx_id = ctx_drvdata->num;
|
|
map.info.va = va;
|
|
map.info.size = len;
|
|
|
|
if (sg->length == len) {
|
|
/*
|
|
* physical address for secure mapping needs
|
|
* to be 1MB aligned
|
|
*/
|
|
pa = get_phys_addr(sg);
|
|
if (!IS_ALIGNED(pa, SZ_1M))
|
|
return -EINVAL;
|
|
map.plist.list = virt_to_phys(&pa);
|
|
map.plist.list_size = 1;
|
|
map.plist.size = len;
|
|
flush_va = &pa;
|
|
} else {
|
|
sgiter = sg;
|
|
if (!IS_ALIGNED(sgiter->length, SZ_1M))
|
|
return -EINVAL;
|
|
cnt = sg->length / SZ_1M;
|
|
while ((sgiter = sg_next(sgiter))) {
|
|
if (!IS_ALIGNED(sgiter->length, SZ_1M))
|
|
return -EINVAL;
|
|
cnt += sgiter->length / SZ_1M;
|
|
}
|
|
|
|
pa_list = kmalloc_array(cnt, sizeof(*pa_list), GFP_KERNEL);
|
|
if (!pa_list)
|
|
return -ENOMEM;
|
|
|
|
sgiter = sg;
|
|
cnt = 0;
|
|
pa = get_phys_addr(sgiter);
|
|
if (!IS_ALIGNED(pa, SZ_1M)) {
|
|
kfree(pa_list);
|
|
return -EINVAL;
|
|
}
|
|
while (offset < len) {
|
|
pa += chunk_offset;
|
|
pa_list[cnt] = pa;
|
|
chunk_offset += SZ_1M;
|
|
offset += SZ_1M;
|
|
cnt++;
|
|
|
|
if (chunk_offset >= sgiter->length && offset < len) {
|
|
chunk_offset = 0;
|
|
sgiter = sg_next(sgiter);
|
|
pa = get_phys_addr(sgiter);
|
|
}
|
|
}
|
|
|
|
map.plist.list = virt_to_phys(pa_list);
|
|
map.plist.list_size = cnt;
|
|
map.plist.size = SZ_1M;
|
|
flush_va = pa_list;
|
|
}
|
|
|
|
trace_iommu_sec_ptbl_map_range_start(map.info.id, map.info.ctx_id, va,
|
|
pa, len);
|
|
|
|
/*
|
|
* Ensure that the buffer is in RAM by the time it gets to TZ
|
|
*/
|
|
flush_va_end = (void *) (((unsigned long) flush_va) +
|
|
(map.plist.list_size * sizeof(*pa_list)));
|
|
dmac_clean_range(flush_va, flush_va_end);
|
|
|
|
ret = msm_iommu_sec_map2(&map);
|
|
kfree(pa_list);
|
|
|
|
trace_iommu_sec_ptbl_map_range_end(map.info.id, map.info.ctx_id, va, pa,
|
|
len);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int msm_iommu_sec_ptbl_unmap(struct msm_iommu_drvdata *iommu_drvdata,
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata,
|
|
unsigned long va, size_t len)
|
|
{
|
|
struct msm_scm_unmap2_req unmap;
|
|
int ret, scm_ret;
|
|
struct scm_desc desc = {0};
|
|
|
|
if (!IS_ALIGNED(va, SZ_1M) || !IS_ALIGNED(len, SZ_1M))
|
|
return -EINVAL;
|
|
desc.args[0] = unmap.info.id = iommu_drvdata->sec_id;
|
|
desc.args[1] = unmap.info.ctx_id = ctx_drvdata->num;
|
|
desc.args[2] = unmap.info.va = va;
|
|
desc.args[3] = unmap.info.size = len;
|
|
desc.args[4] = unmap.flags = IOMMU_TLBINVAL_FLAG;
|
|
desc.arginfo = SCM_ARGS(5);
|
|
|
|
if (!is_scm_armv8())
|
|
ret = scm_call(SCM_SVC_MP, IOMMU_SECURE_UNMAP2, &unmap,
|
|
sizeof(unmap), &scm_ret, sizeof(scm_ret));
|
|
else
|
|
ret = scm_call2(SCM_SIP_FNID(SCM_SVC_MP,
|
|
IOMMU_SECURE_UNMAP2_FLAT), &desc);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int msm_iommu_domain_init(struct iommu_domain *domain)
|
|
{
|
|
struct msm_iommu_priv *priv;
|
|
|
|
priv = kzalloc(sizeof(*priv), GFP_KERNEL);
|
|
if (!priv)
|
|
return -ENOMEM;
|
|
|
|
INIT_LIST_HEAD(&priv->list_attached);
|
|
domain->priv = priv;
|
|
return 0;
|
|
}
|
|
|
|
static void msm_iommu_domain_destroy(struct iommu_domain *domain)
|
|
{
|
|
struct msm_iommu_priv *priv;
|
|
|
|
iommu_access_ops->iommu_lock_acquire(0);
|
|
priv = domain->priv;
|
|
domain->priv = NULL;
|
|
|
|
kfree(priv);
|
|
iommu_access_ops->iommu_lock_release(0);
|
|
}
|
|
|
|
static int msm_iommu_attach_dev(struct iommu_domain *domain, struct device *dev)
|
|
{
|
|
struct msm_iommu_priv *priv;
|
|
struct msm_iommu_drvdata *iommu_drvdata;
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata;
|
|
struct msm_iommu_ctx_drvdata *tmp_drvdata;
|
|
int ret = 0;
|
|
|
|
iommu_access_ops->iommu_lock_acquire(0);
|
|
|
|
priv = domain->priv;
|
|
if (!priv || !dev) {
|
|
ret = -EINVAL;
|
|
goto fail;
|
|
}
|
|
|
|
iommu_drvdata = dev_get_drvdata(dev->parent);
|
|
ctx_drvdata = dev_get_drvdata(dev);
|
|
if (!iommu_drvdata || !ctx_drvdata) {
|
|
ret = -EINVAL;
|
|
goto fail;
|
|
}
|
|
|
|
if (!list_empty(&ctx_drvdata->attached_elm)) {
|
|
ret = -EBUSY;
|
|
goto fail;
|
|
}
|
|
|
|
list_for_each_entry(tmp_drvdata, &priv->list_attached, attached_elm)
|
|
if (tmp_drvdata == ctx_drvdata) {
|
|
ret = -EBUSY;
|
|
goto fail;
|
|
}
|
|
|
|
ret = iommu_access_ops->iommu_power_on(iommu_drvdata);
|
|
if (ret)
|
|
goto fail;
|
|
|
|
/* We can only do this once */
|
|
if (!iommu_drvdata->ctx_attach_count) {
|
|
ret = iommu_access_ops->iommu_clk_on(iommu_drvdata);
|
|
if (ret) {
|
|
iommu_access_ops->iommu_power_off(iommu_drvdata);
|
|
goto fail;
|
|
}
|
|
|
|
ret = msm_iommu_sec_program_iommu(iommu_drvdata,
|
|
ctx_drvdata);
|
|
|
|
/* bfb settings are always programmed by HLOS */
|
|
program_iommu_bfb_settings(iommu_drvdata->base,
|
|
iommu_drvdata->bfb_settings);
|
|
|
|
iommu_access_ops->iommu_clk_off(iommu_drvdata);
|
|
if (ret) {
|
|
iommu_access_ops->iommu_power_off(iommu_drvdata);
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
list_add(&(ctx_drvdata->attached_elm), &priv->list_attached);
|
|
ctx_drvdata->attached_domain = domain;
|
|
++iommu_drvdata->ctx_attach_count;
|
|
|
|
iommu_access_ops->iommu_lock_release(0);
|
|
|
|
msm_iommu_attached(dev->parent);
|
|
return ret;
|
|
fail:
|
|
iommu_access_ops->iommu_lock_release(0);
|
|
return ret;
|
|
}
|
|
|
|
static void msm_iommu_detach_dev(struct iommu_domain *domain,
|
|
struct device *dev)
|
|
{
|
|
struct msm_iommu_drvdata *iommu_drvdata;
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata;
|
|
|
|
if (!dev)
|
|
return;
|
|
|
|
msm_iommu_detached(dev->parent);
|
|
|
|
iommu_access_ops->iommu_lock_acquire(0);
|
|
|
|
iommu_drvdata = dev_get_drvdata(dev->parent);
|
|
ctx_drvdata = dev_get_drvdata(dev);
|
|
if (!iommu_drvdata || !ctx_drvdata || !ctx_drvdata->attached_domain)
|
|
goto fail;
|
|
|
|
list_del_init(&ctx_drvdata->attached_elm);
|
|
ctx_drvdata->attached_domain = NULL;
|
|
|
|
iommu_access_ops->iommu_power_off(iommu_drvdata);
|
|
BUG_ON(iommu_drvdata->ctx_attach_count == 0);
|
|
--iommu_drvdata->ctx_attach_count;
|
|
fail:
|
|
iommu_access_ops->iommu_lock_release(0);
|
|
}
|
|
|
|
static int get_drvdata(struct iommu_domain *domain,
|
|
struct msm_iommu_drvdata **iommu_drvdata,
|
|
struct msm_iommu_ctx_drvdata **ctx_drvdata)
|
|
{
|
|
struct msm_iommu_priv *priv = domain->priv;
|
|
struct msm_iommu_ctx_drvdata *ctx;
|
|
|
|
list_for_each_entry(ctx, &priv->list_attached, attached_elm) {
|
|
if (ctx->attached_domain == domain)
|
|
break;
|
|
}
|
|
|
|
if (ctx->attached_domain != domain)
|
|
return -EINVAL;
|
|
|
|
*ctx_drvdata = ctx;
|
|
*iommu_drvdata = dev_get_drvdata(ctx->pdev->dev.parent);
|
|
return 0;
|
|
}
|
|
|
|
static int msm_iommu_map(struct iommu_domain *domain, unsigned long va,
|
|
phys_addr_t pa, size_t len, int prot)
|
|
{
|
|
struct msm_iommu_drvdata *iommu_drvdata;
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata;
|
|
int ret = 0;
|
|
|
|
iommu_access_ops->iommu_lock_acquire(0);
|
|
|
|
ret = get_drvdata(domain, &iommu_drvdata, &ctx_drvdata);
|
|
if (ret)
|
|
goto fail;
|
|
|
|
iommu_access_ops->iommu_clk_on(iommu_drvdata);
|
|
ret = msm_iommu_sec_ptbl_map(iommu_drvdata, ctx_drvdata,
|
|
va, pa, len);
|
|
iommu_access_ops->iommu_clk_off(iommu_drvdata);
|
|
fail:
|
|
iommu_access_ops->iommu_lock_release(0);
|
|
return ret;
|
|
}
|
|
|
|
static size_t msm_iommu_unmap(struct iommu_domain *domain, unsigned long va,
|
|
size_t len)
|
|
{
|
|
struct msm_iommu_drvdata *iommu_drvdata;
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata;
|
|
int ret = -ENODEV;
|
|
|
|
iommu_access_ops->iommu_lock_acquire(0);
|
|
|
|
ret = get_drvdata(domain, &iommu_drvdata, &ctx_drvdata);
|
|
if (ret)
|
|
goto fail;
|
|
|
|
iommu_access_ops->iommu_clk_on(iommu_drvdata);
|
|
ret = msm_iommu_sec_ptbl_unmap(iommu_drvdata, ctx_drvdata,
|
|
va, len);
|
|
iommu_access_ops->iommu_clk_off(iommu_drvdata);
|
|
fail:
|
|
iommu_access_ops->iommu_lock_release(0);
|
|
|
|
/* the IOMMU API requires us to return how many bytes were unmapped */
|
|
len = ret ? 0 : len;
|
|
return len;
|
|
}
|
|
|
|
static int msm_iommu_map_range(struct iommu_domain *domain, unsigned long va,
|
|
struct scatterlist *sg, size_t len,
|
|
int prot)
|
|
{
|
|
int ret;
|
|
struct msm_iommu_drvdata *iommu_drvdata;
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata;
|
|
|
|
iommu_access_ops->iommu_lock_acquire(0);
|
|
|
|
ret = get_drvdata(domain, &iommu_drvdata, &ctx_drvdata);
|
|
if (ret)
|
|
goto fail;
|
|
iommu_access_ops->iommu_clk_on(iommu_drvdata);
|
|
ret = msm_iommu_sec_ptbl_map_range(iommu_drvdata, ctx_drvdata,
|
|
va, sg, len);
|
|
iommu_access_ops->iommu_clk_off(iommu_drvdata);
|
|
fail:
|
|
iommu_access_ops->iommu_lock_release(0);
|
|
return ret;
|
|
}
|
|
|
|
static size_t msm_iommu_map_sg(struct iommu_domain *domain, unsigned long va,
|
|
struct scatterlist *sg, unsigned int nr_entries,
|
|
int prot)
|
|
{
|
|
int ret, i;
|
|
struct scatterlist *tmp;
|
|
unsigned long len = 0;
|
|
|
|
/*
|
|
* Longer term work: convert over to generic page table management
|
|
* which means we can work on scattergather lists and the whole range
|
|
*/
|
|
for_each_sg(sg, tmp, nr_entries, i)
|
|
len += tmp->length;
|
|
|
|
ret = msm_iommu_map_range(domain, va, sg, len, prot);
|
|
if (ret)
|
|
return 0;
|
|
else
|
|
return len;
|
|
}
|
|
|
|
static int msm_iommu_unmap_range(struct iommu_domain *domain, unsigned long va,
|
|
size_t len)
|
|
{
|
|
struct msm_iommu_drvdata *iommu_drvdata;
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata;
|
|
int ret = -EINVAL;
|
|
|
|
if (!IS_ALIGNED(va, SZ_1M) || !IS_ALIGNED(len, SZ_1M))
|
|
return -EINVAL;
|
|
|
|
iommu_access_ops->iommu_lock_acquire(0);
|
|
|
|
ret = get_drvdata(domain, &iommu_drvdata, &ctx_drvdata);
|
|
if (ret)
|
|
goto fail;
|
|
|
|
iommu_access_ops->iommu_clk_on(iommu_drvdata);
|
|
ret = msm_iommu_sec_ptbl_unmap(iommu_drvdata, ctx_drvdata, va, len);
|
|
iommu_access_ops->iommu_clk_off(iommu_drvdata);
|
|
|
|
fail:
|
|
iommu_access_ops->iommu_lock_release(0);
|
|
return ret ? ret : 0;
|
|
}
|
|
|
|
static phys_addr_t msm_iommu_iova_to_phys(struct iommu_domain *domain,
|
|
phys_addr_t va)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static bool msm_iommu_capable(enum iommu_cap cap)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void msm_iommu_check_scm_call_avail(void)
|
|
{
|
|
is_secure = scm_is_call_available(SCM_SVC_MP, IOMMU_SECURE_CFG);
|
|
}
|
|
|
|
int msm_iommu_get_scm_call_avail(void)
|
|
{
|
|
return is_secure;
|
|
}
|
|
|
|
/*
|
|
* VFE SMMU is changing from being non-secure to being secure.
|
|
* For backwards compatibility we need to check whether the secure environment
|
|
* has support for this.
|
|
*/
|
|
static s32 secure_camera_enabled = -1;
|
|
int is_vfe_secure(void)
|
|
{
|
|
if (secure_camera_enabled == -1) {
|
|
u32 ver = scm_get_feat_version(SCM_SVC_SEC_CAMERA);
|
|
|
|
secure_camera_enabled = ver >= MAKE_VERSION(1, 0, 0);
|
|
}
|
|
return secure_camera_enabled;
|
|
}
|
|
|
|
static int msm_iommu_dma_supported(struct iommu_domain *domain,
|
|
struct device *dev, u64 mask)
|
|
{
|
|
return ((1ULL << 32) - 1) < mask ? 0 : 1;
|
|
}
|
|
|
|
static int msm_iommu_domain_set_attr(struct iommu_domain *domain,
|
|
enum iommu_attr attr, void *data)
|
|
{
|
|
switch (attr) {
|
|
case DOMAIN_ATTR_COHERENT_HTW_DISABLE:
|
|
/*
|
|
* Just quietly bail out as L2-redirect feature
|
|
* cannot be enabled for Secure context banks.
|
|
*/
|
|
break;
|
|
case DOMAIN_ATTR_SECURE_VMID:
|
|
/*
|
|
* MSM iommu driver doesn't set the VMID for
|
|
* any domain.
|
|
*/
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int msm_iommu_domain_get_attr(struct iommu_domain *domain,
|
|
enum iommu_attr attr, void *data)
|
|
{
|
|
struct msm_iommu_priv *priv = domain->priv;
|
|
struct msm_iommu_ctx_drvdata *ctx_drvdata;
|
|
|
|
switch (attr) {
|
|
case DOMAIN_ATTR_COHERENT_HTW_DISABLE:
|
|
/*
|
|
* This is the case always for secure
|
|
* context banks
|
|
*/
|
|
*((unsigned int *) data) = 1;
|
|
break;
|
|
case DOMAIN_ATTR_SECURE_VMID:
|
|
*((int *) data) = -VMID_INVAL;
|
|
break;
|
|
case DOMAIN_ATTR_CONTEXT_BANK:
|
|
if (list_empty(&priv->list_attached))
|
|
return -ENODEV;
|
|
|
|
ctx_drvdata = list_first_entry(&priv->list_attached,
|
|
struct msm_iommu_ctx_drvdata, attached_elm);
|
|
*((unsigned int *) data) = ctx_drvdata->num;
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static struct iommu_ops msm_iommu_ops = {
|
|
.domain_init = msm_iommu_domain_init,
|
|
.domain_destroy = msm_iommu_domain_destroy,
|
|
.attach_dev = msm_iommu_attach_dev,
|
|
.detach_dev = msm_iommu_detach_dev,
|
|
.map = msm_iommu_map,
|
|
.unmap = msm_iommu_unmap,
|
|
.map_range = msm_iommu_map_range,
|
|
.map_sg = msm_iommu_map_sg,
|
|
.unmap_range = msm_iommu_unmap_range,
|
|
.iova_to_phys = msm_iommu_iova_to_phys,
|
|
.capable = msm_iommu_capable,
|
|
.pgsize_bitmap = MSM_IOMMU_PGSIZES,
|
|
.domain_set_attr = msm_iommu_domain_set_attr,
|
|
.domain_get_attr = msm_iommu_domain_get_attr,
|
|
.dma_supported = msm_iommu_dma_supported,
|
|
};
|
|
|
|
static int __init msm_iommu_sec_init(void)
|
|
{
|
|
int ret;
|
|
|
|
ret = bus_register(&msm_iommu_sec_bus_type);
|
|
if (ret)
|
|
goto fail;
|
|
|
|
bus_set_iommu(&msm_iommu_sec_bus_type, &msm_iommu_ops);
|
|
ret = msm_iommu_sec_ptbl_init();
|
|
fail:
|
|
return ret;
|
|
}
|
|
|
|
subsys_initcall(msm_iommu_sec_init);
|
|
|
|
MODULE_LICENSE("GPL v2");
|
|
MODULE_DESCRIPTION("MSM SMMU Secure Driver");
|