// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2020 MediaTek Inc.
 */

#include <asm/page.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/device.h>
#include <linux/compiler.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/arm-smccc.h>
#include <linux/soc/mediatek/mtk_sip_svc.h>
#include "devmpu.h"

#define LOG_TAG "[DEVMPU]"

#define DEVMPU_MAX_TAG_LEN 15
#define DUMP_TAG  "dump_devmpu"
#define CHECK_TAG "violate_devmpu"

#define DEVMPU_MAX_CTX 3
#define DEVMPU_DEFAULT_CTX 0

#ifdef pr_fmt
#undef pr_fmt
#endif
#define pr_fmt(fmt) LOG_TAG " " fmt

#define switchValue(x) (((x << 24) & 0xff000000) | \
			((x << 8) & 0x00ff0000)  | \
			((x >> 8) & 0x0000ff00)  | \
			((x >> 24) & 0x000000ff))

struct devmpu_context {

	/* HW register mapped base */
	void __iomem *reg_base;

	/* DRAM (PA) space protected */
	uint64_t prot_base;
	uint64_t prot_size;

	/* page granularity */
	uint32_t page_size;

	/* virtual irq number */
	uint32_t virq;

	/* max ctx */
	uint32_t max_ctx;
} devmpu_ctx_ary[DEVMPU_MAX_CTX];

struct devmpu_vio_stat {

	/* master ID */
	uint16_t id;

	/* master domain */
	uint8_t domain;

	/* is NS transaction (AxPROT[1]) */
	bool is_ns;

	/* is write violation */
	bool is_write;

	/* padding */
	uint8_t padding[3];

	/* physical address */
	uint64_t addr;
};

static int devmpu_vio_get(struct devmpu_vio_stat *vio, bool do_clear)
{
	struct arm_smccc_res res;

	size_t vio_addr;
	size_t vio_info;

	if (unlikely(vio == NULL)) {
		pr_err("%s:%d output pointer is NULL\n",
				__func__, __LINE__);
		return -1;
	}


	arm_smccc_smc(MTK_SIP_KERNEL_DEVMPU_VIO_GET,
			do_clear, 0, 0, 0, 0, 0, 0, &res);
	if (res.a0) {
		pr_err("%s:%d failed to get violation, ret=0x%lx\n",
				__func__, __LINE__, res.a0);
		return -1;
	}

	vio_addr = res.a1;
	vio_info = res.a2;

	vio->addr = vio_addr;
	vio->is_write = (vio_info >> 0) & 0x1;
	vio->is_ns = (vio_info >> 1) & 0x1;
	vio->domain = (vio_info >> 8) & 0xFF;
	vio->id = (vio_info >> 16) & 0xFFFF;

	return 0;
}

static int devmpu_rw_perm_get(uint64_t pa, size_t *rd_perm, size_t *wr_perm)
{
	struct arm_smccc_res res;
	struct devmpu_context *devmpu_ctx;

	devmpu_ctx = &devmpu_ctx_ary[DEVMPU_DEFAULT_CTX];

	if (unlikely(
			pa < devmpu_ctx->prot_base
		||	pa >= devmpu_ctx->prot_base + devmpu_ctx->prot_size)) {
		pr_err("%s:%d invalid DRAM physical address, pa=0x%llx\n",
				__func__, __LINE__, pa);
		return -1;
	}

	if (unlikely(rd_perm == NULL || wr_perm == NULL)) {
		pr_err("%s:%d output pointer is NULL\n",
				__func__, __LINE__);
		return -1;
	}

	arm_smccc_smc(MTK_SIP_KERNEL_DEVMPU_PERM_GET,
			pa, 0, 0, 0, 0, 0, 0, &res);
	if (res.a0) {
		pr_err("%s:%d failed to get permission, ret=0x%lx\n",
				__func__, __LINE__, res.a0);
		return -1;
	}

	*rd_perm = (size_t)res.a1;
	*wr_perm = (size_t)res.a2;

	return 0;
}

int devmpu_print_violation(uint64_t vio_addr, uint32_t vio_id,
		uint32_t vio_domain, uint32_t vio_rw)
{
	size_t ret;
	struct devmpu_context *devmpu_ctx;
	struct devmpu_vio_stat vio = {
		.id = 0,
		.domain = 0,
		.is_ns = false,
		.is_write = false,
		.addr = 0x0ULL
	};

	uint32_t vio_axi_id;
	uint32_t vio_port_id;
	uint32_t page;
	size_t rd_perm;
	size_t wr_perm;

	devmpu_ctx = &devmpu_ctx_ary[DEVMPU_DEFAULT_CTX];

	/* overwrite violation info. with the DeviceMPU native one */
	ret = devmpu_vio_get(&vio, true);
	if (ret) {
		pr_err("%s:%d failed to get DeviceMPU violation\n",
				__func__, __LINE__);
		return -1;
	}

	vio_id = vio.id;
	vio_addr = vio.addr;
	vio_domain = vio.domain;

	/*
	 * use 0b01/0b10 to specify write/read violation
	 */
	vio_rw = (vio.is_write) ? 1 : 2;

	vio_addr += devmpu_ctx->prot_base;

	vio_axi_id = (vio_id >> 3) & 0x1FFF;
	vio_port_id = vio_id & 0x7;

	pr_info("Device MPU violation\n");
	pr_info("current process is \"%s \" (pid: %i)\n",
			current->comm, current->pid);
	pr_info("corrupted address is 0x%llx\n",
			vio_addr);
	pr_info("master ID: 0x%x, AXI ID: 0x%x, port ID: 0x%x\n",
			vio_id, vio_axi_id, vio_port_id);
	pr_info("violation domain 0x%x\n",
			vio_domain);

	if (vio_rw == 1)
		pr_info("write violation\n");
	else if (vio_rw == 2)
		pr_info("read violation\n");
	else
		pr_info("strange read/write violation (%u)\n", vio_rw);

	if (!devmpu_rw_perm_get(vio_addr, &rd_perm, &wr_perm)) {
		page = vio_addr - devmpu_ctx->prot_base;
		page /= devmpu_ctx->page_size;
		pr_info("Page#%x RD/WR : %08zx/%08zx (%lld)\n",
			page,
			switchValue(rd_perm),
			switchValue(wr_perm),
			(vio_addr / devmpu_ctx->page_size) % 4);
	}

	pr_info("%s transaction\n",
			(vio.is_ns) ? "non-secure" : "secure");

	return 0;
}
EXPORT_SYMBOL(devmpu_print_violation);

#if IS_ENABLED(CONFIG_MTK_DEVMPU_DEBUG)
/* sysfs */
static int devmpu_dump_perm(void)
{
	struct devmpu_context *devmpu_ctx;

	uint32_t i;

	uint64_t pa, pa_dump;
	uint32_t pages;

	size_t rd_perm;
	size_t wr_perm;

	uint8_t rd_perm_bmp[16];
	uint8_t wr_perm_bmp[16];

	devmpu_ctx = &devmpu_ctx_ary[DEVMPU_DEFAULT_CTX];

	pa = devmpu_ctx->prot_base;
	pages = devmpu_ctx->prot_size / devmpu_ctx->page_size;

	pr_info("Page# (bus-addr)  :  RD/WR permissions\n");

	for (i = 0; i < pages; ++i) {
		if (i && i % 16 == 0) {
			pa_dump = (uint64_t)(i - 16) * devmpu_ctx->page_size;
			pa_dump += devmpu_ctx->prot_base;
			pr_info("%04x (%02x_%08x):  %08x/%08x %08x/%08x %08x/%08x %08x/%08x\n",
				i - 16,
				(uint32_t)(pa_dump >> 32),
				(uint32_t)(pa_dump & 0xffffffff),
				*((uint32_t *)rd_perm_bmp),
				*((uint32_t *)wr_perm_bmp),
				*((uint32_t *)rd_perm_bmp+1),
				*((uint32_t *)wr_perm_bmp+1),
				*((uint32_t *)rd_perm_bmp+2),
				*((uint32_t *)wr_perm_bmp+2),
				*((uint32_t *)rd_perm_bmp+3),
				*((uint32_t *)wr_perm_bmp+3));
		}

		if (devmpu_rw_perm_get(pa, &rd_perm, &wr_perm)) {
			pr_err("%s:%d failed to get permission\n",
					__func__, __LINE__);
			return -EINVAL;
		}

		rd_perm_bmp[i % 16] = (uint8_t)rd_perm;
		wr_perm_bmp[i % 16] = (uint8_t)wr_perm;

		pa += devmpu_ctx->page_size;
	}

	return 0;
}

static int devmpu_check_violation(void)
{
	struct devmpu_context *devmpu_ctx;
	struct device_node *dn;
	void __iomem *reg_base;
	uint32_t prop_ary[4];
	uint32_t prop_addr;
	uint32_t prop_size;
	uint64_t prop_value;

	devmpu_ctx = &devmpu_ctx_ary[DEVMPU_DEFAULT_CTX];

	/* Get property from dts */
	dn = of_find_compatible_node(NULL, NULL, "mediatek,atf-reserved");
	if (!dn) {
		pr_err("%s:%d failed to get device node\n",
				__func__, __LINE__);
		return -EINVAL;
	}

	if (of_property_read_u32_array(dn, "reg", prop_ary, 4)) {
		pr_err("%s:%d failed to get property\n",
				__func__, __LINE__);
		return -EINVAL;
	}

	prop_addr = prop_ary[1];
	prop_size = prop_ary[3];

	pr_info("Address 0x%x Size 0x%x\n", prop_addr, prop_size);

	/* Check if 2MB addr/size alignment */
	if (prop_addr % devmpu_ctx->page_size) {
		pr_err("%s:%d address is not 2MB alignment 0x%x\n",
				__func__, __LINE__, prop_addr);
		return -EINVAL;
	}

	if (prop_size % devmpu_ctx->page_size) {
		pr_err("%s:%d size is not 2MB alignment 0x%x\n",
				__func__, __LINE__, prop_size);
		return -EINVAL;
	}

	pr_info("Check 2MB address/size alignment correctly\n");

	/* Trigger DevMPU violation */
	if (prop_addr && prop_size) {
		pr_info("Check if DevMPU violation is at 0x%x\n", prop_addr);
		reg_base = ioremap((phys_addr_t)prop_addr, prop_size);
		if (!reg_base)
			return -EIO;

		pr_info("Read from 0x%pK\n", reg_base);

		prop_value = readq(reg_base);
		pr_info("%s:%d value 0x%llx\n", __func__, __LINE__, prop_value);

		if (prop_value) {
			pr_info("%s:%d EMI didn't protect READ and check HP mode\n",
				__func__, __LINE__);
			BUG();
		}

		pr_info("Write to 0x%pK\n", reg_base);
		writeq(prop_value | 0x5AA5AA55, reg_base);

		prop_value = readq(reg_base);
		pr_info("%s:%d value 0x%llx\n", __func__, __LINE__, prop_value);

		if (prop_value) {
			pr_info("%s:%d EMI didn't protect WRITE and check HP mode\n",
				__func__, __LINE__);
			BUG();
		}

		iounmap(reg_base);
	}

	return 0;
}

static ssize_t devmpu_config_show(struct device_driver *driver, char *buf)
{
	return 0;
}

static ssize_t devmpu_config_store(struct device_driver *driver,
	const char *buf, size_t count)
{
	int ret = 0;

	if (strlen(buf) > DEVMPU_MAX_TAG_LEN) {
		pr_notice("%s: command overflow\n", __func__);
		return -EINVAL;
	}

	if (!strncmp(buf, DUMP_TAG, strlen(DUMP_TAG))) {
		ret = devmpu_dump_perm();
	}	else if (!strncmp(buf, CHECK_TAG, strlen(CHECK_TAG))) {
		ret = devmpu_check_violation();
	} else {
		pr_notice("%s Invalid argument!!\n", __func__);
		return -EINVAL;
	}

	if (ret) {
		pr_notice("%s verificaiton fail!!\n", __func__);
		return -EINVAL;
	}

	return count;
}
static DRIVER_ATTR_RW(devmpu_config);
#endif

static irqreturn_t devmpu_irq_handler(int irq, void *dev_id)
{
	devmpu_print_violation(0, 0, 0, 0);
	return IRQ_HANDLED;
}

/* driver registration */
static int devmpu_probe(struct platform_device *pdev)
{
	int rc;

	void __iomem *reg_base;
	static uint32_t probe_cnt;
	struct devmpu_context *devmpu_ctx = NULL;
	uint64_t prot_base;
	uint64_t prot_size;
	uint32_t page_size;
	uint32_t virq;

	struct device_node *dn = pdev->dev.of_node;
	struct resource *res = NULL;

	pr_info("Device MPU probe: %s\n", pdev->name);

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res) {
		pr_err("%s:%d failed to get resource\n",
				__func__, __LINE__);
		return -ENOENT;
	}

	reg_base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(reg_base)) {
		pr_err("%s:%d unable to map DEVMPU_BASE\n",
				__func__, __LINE__);
		return -ENOENT;
	}

	if (of_property_read_u64(dn, "prot-base", &prot_base)) {
		pr_err("%s:%d failed to get protected region base\n",
				__func__, __LINE__);
		return -ENOENT;
	}

	if (of_property_read_u64(dn, "prot-size", &prot_size)) {
		pr_err("%s:%d failed to get protected region size\n",
				__func__, __LINE__);
		return -ENOENT;
	}

	if (of_property_read_u32(dn, "page-size", &page_size)) {
		pr_err("%s:%d failed to get protected region granularity\n",
				__func__, __LINE__);
		return -ENOENT;
	}

	virq = irq_of_parse_and_map(dn, 0);
	rc = request_irq(virq, (irq_handler_t)devmpu_irq_handler,
			IRQF_TRIGGER_NONE, "devmpu", NULL);
	if (rc) {
		pr_err("%s:%d failed to request irq, rc=%d\n",
				__func__, __LINE__, rc);
		return -EPERM;
	}

	if ((probe_cnt + 1) > DEVMPU_MAX_CTX) {
		pr_err("%s:%d failed to create context\n",
				__func__, __LINE__);
		return -EINVAL;
	}

	devmpu_ctx = &devmpu_ctx_ary[probe_cnt];

	devmpu_ctx->reg_base = reg_base;
	devmpu_ctx->prot_base = prot_base;
	devmpu_ctx->prot_size = prot_size;
	devmpu_ctx->page_size = page_size;
	devmpu_ctx->virq = virq;
	devmpu_ctx->max_ctx = (probe_cnt + 1);

	pr_info("Create context [%d]\n", probe_cnt);
	pr_info("reg_base=0x%pK\n", devmpu_ctx->reg_base);
	pr_info("prot_base=0x%llx\n", devmpu_ctx->prot_base);
	pr_info("prot_size=0x%llx\n", devmpu_ctx->prot_size);
	pr_info("page_size=0x%x\n", devmpu_ctx->page_size);
	pr_info("virq=0x%x\n", devmpu_ctx->virq);

	probe_cnt++;

	return 0;
}

static const struct of_device_id devmpu_of_match[] = {
	{ .compatible = "mediatek,device_mpu_low" },
	{ .compatible = "mediatek,device_mpu_sub" },
	{ .compatible = "mediatek,device_mpu_acp" },
	{},
};

static struct platform_driver devmpu_drv = {
	.probe = devmpu_probe,
	.driver = {
		.name = "devmpu",
		.owner = THIS_MODULE,
		.of_match_table = devmpu_of_match,
	},
};

static int __init devmpu_init(void)
{
	int ret = 0;

	ret = platform_driver_register(&devmpu_drv);
	if (ret) {
		pr_err("%s:%d failed to register devmpu driver, ret=%d\n",
				__func__, __LINE__, ret);
	}

#if IS_ENABLED(CONFIG_MTK_DEVMPU_DEBUG)
	ret = driver_create_file(&devmpu_drv.driver,
			&driver_attr_devmpu_config);
	if (ret) {
		pr_err("%s:%d failed to create driver sysfs file, ret=%d\n",
				__func__, __LINE__, ret);
	}
#endif

	return ret;
}

static void __exit devmpu_exit(void)
{
#if IS_ENABLED(CONFIG_MTK_DEVMPU_DEBUG)
	driver_remove_file(&devmpu_drv.driver,
			&driver_attr_devmpu_config);
#endif

	platform_driver_unregister(&devmpu_drv);
}

module_init(devmpu_init);
module_exit(devmpu_exit);

MODULE_DESCRIPTION("Mediatek Device MPU Driver");
MODULE_AUTHOR("Calvin Liao <calvin.liao@mediatek.com>");
MODULE_LICENSE("GPL");
