// SPDX-License-Identifier: GPL-2.0-only
/*
 * sec_debug_test.c
 *
 * author: Myoungae Kim
 * email: myoungjae.kim@samsung.com
 *
 * Copyright (c) 2019 Samsung Electronics Co., Ltd
 *              http://www.samsung.com
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/cpu.h>
#include <linux/io.h>
#include <linux/slab.h>
#include <asm-generic/io.h>
#include <linux/ctype.h>
#include <linux/sec_debug.h>
#include <linux/kthread.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/preempt.h>
#include <linux/rwsem.h>
#include <linux/moduleparam.h>
#include <linux/cpumask.h>
#include <linux/reboot.h>
#include <asm/stackprotector.h>

#include <soc/samsung/debug-snapshot.h>
#if IS_ENABLED(CONFIG_SOC_S5E3830)
#include <soc/samsung/exynos-pmu.h>
#else
#include <soc/samsung/exynos-pmu-if.h>
#endif
#include <soc/samsung/exynos_pm_qos.h>
#include <uapi/linux/sched/types.h>

#include <asm/hw_breakpoint.h>
#include <linux/perf_event.h>
#include <linux/hw_breakpoint.h>
#include <uapi/linux/hw_breakpoint.h>

#include <linux/pid.h>
#include <linux/rculist.h>
#include <linux/sched/task.h>

#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <linux/errno.h>

#include "sec_debug_internal.h"

#ifdef pr_fmt
#undef pr_fmt
#endif
#define pr_fmt(fmt) "[sec_wp] " fmt

#define WATCHER_TEST			0

#define WATCHER_OPT_OPMODE			0x1
#define WATCHER_OPT_ADDR			0x2
#define WATCHER_OPT_RWMODE			0x4
#define WATCHER_OPT_PID				0x8
#define WATCHER_OPT_NOTIFY			0x10

#define WP_SIZE_OF_SYMBOL			64

enum {
	SEC_WP_OP_NONE,
	SEC_WP_OP_CLEAR,
	SEC_WP_OP_SET_KERNEL,
	SEC_WP_OP_SET_USER,
};

enum {
	SEC_WP_SUBOP_NONE = 0,
	SEC_WP_SUBOP_TASK,
};

enum {
	SEC_WP_NOTI_NONE,
	SEC_WP_NOTI_PANIC,
	SEC_WP_NOTI_DUMPSTACK,
};

enum {
	SEC_WP_DISABLED,
	SEC_WP_ENABLED_KERNEL,
	SEC_WP_ENABLED_USER,
};

#define SEC_WP_MD_READ			1
#define SEC_WP_MD_WRITE			2


struct __input_env {
	int op;
	int sub_op;
	u64 addr;
	char symbol[WP_SIZE_OF_SYMBOL];
	int rw_mode;
	pid_t pid;
	int noti_type;
	u64 opt_bitmap;
};

static int enable;
static struct perf_event * __percpu *kernel_hbp;
static struct perf_event *user_hbp;

#if WATCHER_TEST
static int test_var;

static int sec_debug_get_test_wp(char *buffer, const struct kernel_param *kp)
{
	int size = 0;

	size = sprintf(buffer, "%lx\n", (unsigned long)&test_var);

	return size;
}

static int sec_debug_set_test_wp(const char *val, const struct kernel_param *kp)
{
	pr_info("access test_var %d\n", test_var);
	test_var = 0;

	return 0;
}

static const struct kernel_param_ops sec_debug_test_wp_ops = {
		.set	= sec_debug_set_test_wp,
		.get	= sec_debug_get_test_wp,
};

module_param_cb(test_wp, &sec_debug_test_wp_ops, NULL, 0600);
#endif

static void __default_hbp_handler(struct perf_event *bp,
			       struct perf_sample_data *data,
			       struct pt_regs *regs)
{
	pr_info("%s\n", __func__);
	BUG();
}

static void __print_args(char **argv, int argc)
{
	int i;

	for (i = 0; i < argc; i++)
		pr_info("argv[%d]: %s\n", i, argv[i]);
}

static int __opt_is_same(const char *src, const char *dst)
{
	return !strncmp(src, dst, strlen(dst));
}

static int __parse_input(struct __input_env *input_env, char **argv, int argc)
{
	int ret = 0;
	char *opt_name, *opt_value;
	int opt_offset = 0;

	if (!argv || argc <= 0 || argc % 2 != 0)
		return -1;

	/* input argument parsing */
	do {
		opt_name = argv[opt_offset];
		opt_value = argv[opt_offset+1];

		if (__opt_is_same(opt_name, "-o")) {
			pr_info("-o is given:%s\n", opt_value);
			input_env->opt_bitmap |= WATCHER_OPT_OPMODE;
			switch (opt_value[0]) {
			case 'k':
			case 'K':
				input_env->op = SEC_WP_OP_SET_KERNEL;
				break;
			case 'u':
			case 'U':
				input_env->op = SEC_WP_OP_SET_USER;
				break;
			case 'c':
			case 'C':
				input_env->op = SEC_WP_OP_CLEAR;
				break;
			default:
				pr_info("invalid operation argument\n");
				return -1;
			}
			pr_info("given opt_value is %d\n", input_env->op);
		} else if (__opt_is_same(opt_name, "-m")) {
			pr_info("-m is given:%s\n", opt_value);
			input_env->opt_bitmap |= WATCHER_OPT_RWMODE;

			input_env->rw_mode = 0;
			if (strchr(opt_value, 'r') != NULL)
				input_env->rw_mode |= HW_BREAKPOINT_R;
			if (strchr(opt_value, 'w') != NULL)
				input_env->rw_mode |= HW_BREAKPOINT_W;

			pr_info("given opt_value is %d\n", input_env->rw_mode);
		} else if (__opt_is_same(opt_name, "-a")) {
			u64 addr;
			int ret;

			pr_info("-a is given:%s\n", opt_value);
			input_env->opt_bitmap |= WATCHER_OPT_ADDR;

			ret = kstrtou64(opt_value, 16, &addr);

			if (ret < 0) {
				pr_info("invalid address\n");
				return -1;
			}
			input_env->addr = addr;
			pr_info("given opt_value is %lu\n", (unsigned long)input_env->addr);
		} else if (__opt_is_same(opt_name, "-s")) {
			pr_info("-s is given:%s\n", opt_value);
		} else if (__opt_is_same(opt_name, "-n")) {
			pr_info("-n is given:%s\n", opt_value);
			input_env->opt_bitmap |= WATCHER_OPT_NOTIFY;

			switch (opt_value[0]) {
			case 'p':
			case 'P':
				input_env->noti_type = SEC_WP_NOTI_PANIC;
				break;
			case 'd':
			case 'D':
				input_env->noti_type = SEC_WP_NOTI_DUMPSTACK;
				break;
			default:
				pr_info("invalid notify option\n");
				return -1;
			}
			pr_info("given opt_value is %d\n", input_env->noti_type);
		} else if (__opt_is_same(opt_name, "-p")) {
			u32 pid;
			int ret;

			pr_info("-p is given:%s\n", opt_value);
			input_env->opt_bitmap |= WATCHER_OPT_PID;

			ret = kstrtou32(opt_value, 10, &pid);

			if (ret < 0) {
				pr_info("invalid pid\n");
				return -1;
			}
			input_env->pid = (pid_t)pid;
			input_env->sub_op = SEC_WP_SUBOP_TASK;
		} else {
			pr_info("invalid option is given\n");
		}
		opt_offset += 2;
	} while (opt_offset < argc);

	/* additional invalidation */

	/* operation mode is mandatory */
	if ((input_env->opt_bitmap & WATCHER_OPT_OPMODE) == 0)
		return -1;

	/* SET operation should get along with valid address input */
	if ((input_env->op == SEC_WP_OP_SET_KERNEL
		|| input_env->op == SEC_WP_OP_SET_USER)
		&& (input_env->opt_bitmap & WATCHER_OPT_ADDR) == 0)
		return -1;

	return ret;
}

static ssize_t sec_debug_watchpoint_read(struct file *file, char __user *user_buf,
				size_t count, loff_t *ppos)
{
	char buf[30];
	ssize_t ret;

	ret = snprintf(buf, sizeof(buf), "%d\n", enable);
	if (ret < 0)
		return ret;

	return simple_read_from_buffer(user_buf, count, ppos, buf, ret);
}

#define SEC_WP_CLI_INPUT_SIZE			200

static ssize_t sec_debug_watchpoint_write(struct file *file, const char __user *user_buf,
				size_t count, loff_t *ppos)
{
	ssize_t ret = count;
	char buf[SEC_WP_CLI_INPUT_SIZE];
	int argc = 0;
	char **argv;
	struct __input_env input_env = {0,};
	struct perf_event_attr attr;

	if (count > SEC_WP_CLI_INPUT_SIZE) {
		pr_info("invalid cmd\n");
		return count;
	}

	copy_from_user(buf, user_buf, count);
	pr_info("cmd:%s\n", buf);

	argv = argv_split(GFP_KERNEL, buf, &argc);

	if (argv == NULL) {
		pr_info("failed to split argument\n");
		goto err;
	}

	__print_args(argv, argc);

	ret = __parse_input(&input_env, argv, argc);
	if (ret < 0) {
		pr_info("failed to parse argument\n");
		goto err;
	}

	/* additional env set */
	if ((input_env.opt_bitmap & WATCHER_OPT_RWMODE) == 0)
		input_env.rw_mode = HW_BREAKPOINT_RW;

	if ((input_env.opt_bitmap & WATCHER_OPT_NOTIFY) == 0)
		input_env.noti_type = SEC_WP_NOTI_PANIC;

	/* watchpoint set */
	if (input_env.op == SEC_WP_OP_SET_KERNEL
		|| input_env.op == SEC_WP_OP_SET_USER) {
		hw_breakpoint_init(&attr);
		attr.bp_addr = (unsigned long)input_env.addr;
		attr.bp_len = HW_BREAKPOINT_LEN_4;
		attr.bp_type = input_env.rw_mode;
	}

	if (input_env.op == SEC_WP_OP_SET_KERNEL) {
		if (enable != SEC_WP_DISABLED) {
			pr_info("watchpoint is already working\n");
			goto err;
		}

		kernel_hbp = register_wide_hw_breakpoint(&attr, __default_hbp_handler, NULL);
		if (IS_ERR((void __force *)kernel_hbp)) {
			ret = PTR_ERR((void __force *)kernel_hbp);
			pr_info("failed to register kernel hbp\n");
			goto err;
		}

		pr_info("kernel watchpoint addr:0x%lx, type:%d\n",
			(unsigned long)input_env.addr, input_env.rw_mode);
		enable = SEC_WP_ENABLED_KERNEL;
	}

	if (input_env.op == SEC_WP_OP_SET_USER) {
		struct task_struct *task = NULL;

		if (enable != SEC_WP_DISABLED) {
			pr_info("watchpoint is already working\n");
			goto err;
		}

		if (input_env.sub_op == SEC_WP_SUBOP_TASK) {
			rcu_read_lock();
			task = find_task_by_vpid(input_env.pid);
			if (task)
				get_task_struct(task);
			rcu_read_unlock();
		} else {
			task = get_current();
		}

		if (!task) {
			pr_info("failed to find task\n");
			goto err;
		}

		user_hbp = register_user_hw_breakpoint(&attr, __default_hbp_handler,
			NULL, task);
		if (IS_ERR((void __force *)user_hbp)) {
			ret = PTR_ERR((void __force *)user_hbp);
			pr_info("failed to register user hbp\n");
			goto err;
		}

		pr_info("user task %s watchpoint addr:0x%lx, type:%d\n",
			task->comm, (unsigned long)input_env.addr, input_env.rw_mode);
		enable = SEC_WP_ENABLED_USER;
	}

	if (input_env.op == SEC_WP_OP_CLEAR) {
		if (enable == SEC_WP_DISABLED) {
			pr_info("sec watcher is not activated\n");
		} else if (enable == SEC_WP_ENABLED_KERNEL && kernel_hbp != NULL) {
			unregister_wide_hw_breakpoint(kernel_hbp);
			kernel_hbp = NULL;
			enable = SEC_WP_DISABLED;
		} else if (enable == SEC_WP_ENABLED_USER && user_hbp != NULL) {
			unregister_hw_breakpoint(user_hbp);
			user_hbp = NULL;
			enable = SEC_WP_DISABLED;
		}
	}

	goto out;
err:
	pr_info("failed to set watchpoint\n");
out:
	pr_info("end of watchpoint\n");
	if (argv)
		argv_free(argv);

	return count;
}

static const struct proc_ops sec_debug_watcher_fops = {
	.proc_open = simple_open,
	.proc_read = sec_debug_watchpoint_read,
	.proc_write = sec_debug_watchpoint_write,
	.proc_lseek = seq_lseek,
};

static int __init sec_debug_watcher_proc_init(void)
{
	pr_info("proc init\n");

	if (!proc_create("sec_debug_watchpoint", 0660, NULL, &sec_debug_watcher_fops)) {
		pr_info("failed to create watchpoint proc node\n");
		goto err;
	}

	pr_info("watcher initialization success\n");
	return 0;

err:
	return -1;
}

late_initcall(sec_debug_watcher_proc_init);

MODULE_DESCRIPTION("Samsung Debug Watchpoint");
MODULE_LICENSE("GPL v2");