/*
 * trace_ksym.c - Kernel Symbol Tracer
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 * Copyright (C) IBM Corporation, 2009
 */

#include <linux/kallsyms.h>
#include <linux/uaccess.h>
#include <linux/debugfs.h>
#include <linux/ftrace.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/fs.h>

#include "trace_output.h"
#include "trace.h"

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

#include <asm/atomic.h>

/*
 * For now, let us restrict the no. of symbols traced simultaneously to number
 * of available hardware breakpoint registers.
 */
#define KSYM_TRACER_MAX HBP_NUM

#define KSYM_TRACER_OP_LEN 3 /* rw- */

struct trace_ksym {
	struct perf_event	**ksym_hbp;
	struct perf_event_attr	attr;
#ifdef CONFIG_PROFILE_KSYM_TRACER
	atomic64_t		counter;
#endif
	struct hlist_node	ksym_hlist;
};

static struct trace_array *ksym_trace_array;

static unsigned int ksym_filter_entry_count;
static unsigned int ksym_tracing_enabled;

static HLIST_HEAD(ksym_filter_head);

static DEFINE_MUTEX(ksym_tracer_mutex);

#ifdef CONFIG_PROFILE_KSYM_TRACER

#define MAX_UL_INT 0xffffffff

void ksym_collect_stats(unsigned long hbp_hit_addr)
{
	struct hlist_node *node;
	struct trace_ksym *entry;

	rcu_read_lock();
	hlist_for_each_entry_rcu(entry, node, &ksym_filter_head, ksym_hlist) {
		if (entry->attr.bp_addr == hbp_hit_addr) {
			atomic64_inc(&entry->counter);
			break;
		}
	}
	rcu_read_unlock();
}
#endif /* CONFIG_PROFILE_KSYM_TRACER */

void ksym_hbp_handler(struct perf_event *hbp, int nmi,
		      struct perf_sample_data *data,
		      struct pt_regs *regs)
{
	struct ring_buffer_event *event;
	struct ksym_trace_entry *entry;
	struct ring_buffer *buffer;
	int pc;

	if (!ksym_tracing_enabled)
		return;

	buffer = ksym_trace_array->buffer;

	pc = preempt_count();

	event = trace_buffer_lock_reserve(buffer, TRACE_KSYM,
							sizeof(*entry), 0, pc);
	if (!event)
		return;

	entry		= ring_buffer_event_data(event);
	entry->ip	= instruction_pointer(regs);
	entry->type	= hw_breakpoint_type(hbp);
	entry->addr	= hw_breakpoint_addr(hbp);
	strlcpy(entry->cmd, current->comm, TASK_COMM_LEN);

#ifdef CONFIG_PROFILE_KSYM_TRACER
	ksym_collect_stats(hw_breakpoint_addr(hbp));
#endif /* CONFIG_PROFILE_KSYM_TRACER */

	trace_buffer_unlock_commit(buffer, event, 0, pc);
}

/* Valid access types are represented as
 *
 * rw- : Set Read/Write Access Breakpoint
 * -w- : Set Write Access Breakpoint
 * --- : Clear Breakpoints
 * --x : Set Execution Break points (Not available yet)
 *
 */
static int ksym_trace_get_access_type(char *str)
{
	int access = 0;

	if (str[0] == 'r')
		access |= HW_BREAKPOINT_R;

	if (str[1] == 'w')
		access |= HW_BREAKPOINT_W;

	if (str[2] == 'x')
		access |= HW_BREAKPOINT_X;

	switch (access) {
	case HW_BREAKPOINT_R:
	case HW_BREAKPOINT_W:
	case HW_BREAKPOINT_W | HW_BREAKPOINT_R:
		return access;
	default:
		return -EINVAL;
	}
}

/*
 * There can be several possible malformed requests and we attempt to capture
 * all of them. We enumerate some of the rules
 * 1. We will not allow kernel symbols with ':' since it is used as a delimiter.
 *    i.e. multiple ':' symbols disallowed. Possible uses are of the form
 *    <module>:<ksym_name>:<op>.
 * 2. No delimiter symbol ':' in the input string
 * 3. Spurious operator symbols or symbols not in their respective positions
 * 4. <ksym_name>:--- i.e. clear breakpoint request when ksym_name not in file
 * 5. Kernel symbol not a part of /proc/kallsyms
 * 6. Duplicate requests
 */
static int parse_ksym_trace_str(char *input_string, char **ksymname,
							unsigned long *addr)
{
	int ret;

	*ksymname = strsep(&input_string, ":");
	*addr = kallsyms_lookup_name(*ksymname);

	/* Check for malformed request: (2), (1) and (5) */
	if ((!input_string) ||
	    (strlen(input_string) != KSYM_TRACER_OP_LEN) ||
	    (*addr == 0))
		return -EINVAL;;

	ret = ksym_trace_get_access_type(input_string);

	return ret;
}

int process_new_ksym_entry(char *ksymname, int op, unsigned long addr)
{
	struct trace_ksym *entry;
	int ret = -ENOMEM;

	if (ksym_filter_entry_count >= KSYM_TRACER_MAX) {
		printk(KERN_ERR "ksym_tracer: Maximum limit:(%d) reached. No"
		" new requests for tracing can be accepted now.\n",
			KSYM_TRACER_MAX);
		return -ENOSPC;
	}

	entry = kzalloc(sizeof(struct trace_ksym), GFP_KERNEL);
	if (!entry)
		return -ENOMEM;

	hw_breakpoint_init(&entry->attr);

	entry->attr.bp_type = op;
	entry->attr.bp_addr = addr;
	entry->attr.bp_len = HW_BREAKPOINT_LEN_4;

	entry->ksym_hbp = register_wide_hw_breakpoint(&entry->attr,
					ksym_hbp_handler);

	if (IS_ERR(entry->ksym_hbp)) {
		ret = PTR_ERR(entry->ksym_hbp);
		printk(KERN_INFO "ksym_tracer request failed. Try again"
					" later!!\n");
		goto err;
	}

	hlist_add_head_rcu(&(entry->ksym_hlist), &ksym_filter_head);
	ksym_filter_entry_count++;

	return 0;

err:
	kfree(entry);

	return ret;
}

static ssize_t ksym_trace_filter_read(struct file *filp, char __user *ubuf,
						size_t count, loff_t *ppos)
{
	struct trace_ksym *entry;
	struct hlist_node *node;
	struct trace_seq *s;
	ssize_t cnt = 0;
	int ret;

	s = kmalloc(sizeof(*s), GFP_KERNEL);
	if (!s)
		return -ENOMEM;
	trace_seq_init(s);

	mutex_lock(&ksym_tracer_mutex);

	hlist_for_each_entry(entry, node, &ksym_filter_head, ksym_hlist) {
		ret = trace_seq_printf(s, "%pS:",
				(void *)(unsigned long)entry->attr.bp_addr);
		if (entry->attr.bp_type == HW_BREAKPOINT_R)
			ret = trace_seq_puts(s, "r--\n");
		else if (entry->attr.bp_type == HW_BREAKPOINT_W)
			ret = trace_seq_puts(s, "-w-\n");
		else if (entry->attr.bp_type == (HW_BREAKPOINT_W | HW_BREAKPOINT_R))
			ret = trace_seq_puts(s, "rw-\n");
		WARN_ON_ONCE(!ret);
	}

	cnt = simple_read_from_buffer(ubuf, count, ppos, s->buffer, s->len);

	mutex_unlock(&ksym_tracer_mutex);

	kfree(s);

	return cnt;
}

static void __ksym_trace_reset(void)
{
	struct trace_ksym *entry;
	struct hlist_node *node, *node1;

	mutex_lock(&ksym_tracer_mutex);
	hlist_for_each_entry_safe(entry, node, node1, &ksym_filter_head,
								ksym_hlist) {
		unregister_wide_hw_breakpoint(entry->ksym_hbp);
		ksym_filter_entry_count--;
		hlist_del_rcu(&(entry->ksym_hlist));
		synchronize_rcu();
		kfree(entry);
	}
	mutex_unlock(&ksym_tracer_mutex);
}

static ssize_t ksym_trace_filter_write(struct file *file,
					const char __user *buffer,
						size_t count, loff_t *ppos)
{
	struct trace_ksym *entry;
	struct hlist_node *node;
	char *buf, *input_string, *ksymname = NULL;
	unsigned long ksym_addr = 0;
	int ret, op, changed = 0;

	buf = kzalloc(count + 1, GFP_KERNEL);
	if (!buf)
		return -ENOMEM;

	ret = -EFAULT;
	if (copy_from_user(buf, buffer, count))
		goto out;

	buf[count] = '\0';
	input_string = strstrip(buf);

	/*
	 * Clear all breakpoints if:
	 * 1: echo > ksym_trace_filter
	 * 2: echo 0 > ksym_trace_filter
	 * 3: echo "*:---" > ksym_trace_filter
	 */
	if (!input_string[0] || !strcmp(input_string, "0") ||
	    !strcmp(input_string, "*:---")) {
		__ksym_trace_reset();
		ret = 0;
		goto out;
	}

	ret = op = parse_ksym_trace_str(input_string, &ksymname, &ksym_addr);
	if (ret < 0)
		goto out;

	mutex_lock(&ksym_tracer_mutex);

	ret = -EINVAL;
	hlist_for_each_entry(entry, node, &ksym_filter_head, ksym_hlist) {
		if (entry->attr.bp_addr == ksym_addr) {
			/* Check for malformed request: (6) */
			if (entry->attr.bp_type != op)
				changed = 1;
			else
				goto out_unlock;
			break;
		}
	}
	if (changed) {
		unregister_wide_hw_breakpoint(entry->ksym_hbp);
		entry->attr.bp_type = op;
		ret = 0;
		if (op > 0) {
			entry->ksym_hbp =
				register_wide_hw_breakpoint(&entry->attr,
					ksym_hbp_handler);
			if (IS_ERR(entry->ksym_hbp))
				ret = PTR_ERR(entry->ksym_hbp);
			else
				goto out_unlock;
		}
		/* Error or "symbol:---" case: drop it */
		ksym_filter_entry_count--;
		hlist_del_rcu(&(entry->ksym_hlist));
		synchronize_rcu();
		kfree(entry);
		goto out_unlock;
	} else {
		/* Check for malformed request: (4) */
		if (op)
			ret = process_new_ksym_entry(ksymname, op, ksym_addr);
	}
out_unlock:
	mutex_unlock(&ksym_tracer_mutex);
out:
	kfree(buf);
	return !ret ? count : ret;
}

static const struct file_operations ksym_tracing_fops = {
	.open		= tracing_open_generic,
	.read		= ksym_trace_filter_read,
	.write		= ksym_trace_filter_write,
};

static void ksym_trace_reset(struct trace_array *tr)
{
	ksym_tracing_enabled = 0;
	__ksym_trace_reset();
}

static int ksym_trace_init(struct trace_array *tr)
{
	int cpu, ret = 0;

	for_each_online_cpu(cpu)
		tracing_reset(tr, cpu);
	ksym_tracing_enabled = 1;
	ksym_trace_array = tr;

	return ret;
}

static void ksym_trace_print_header(struct seq_file *m)
{
	seq_puts(m,
		 "#       TASK-PID   CPU#      Symbol                    "
		 "Type    Function\n");
	seq_puts(m,
		 "#          |        |          |                       "
		 " |         |\n");
}

static enum print_line_t ksym_trace_output(struct trace_iterator *iter)
{
	struct trace_entry *entry = iter->ent;
	struct trace_seq *s = &iter->seq;
	struct ksym_trace_entry *field;
	char str[KSYM_SYMBOL_LEN];
	int ret;

	if (entry->type != TRACE_KSYM)
		return TRACE_TYPE_UNHANDLED;

	trace_assign_type(field, entry);

	ret = trace_seq_printf(s, "%11s-%-5d [%03d] %pS", field->cmd,
				entry->pid, iter->cpu, (char *)field->addr);
	if (!ret)
		return TRACE_TYPE_PARTIAL_LINE;

	switch (field->type) {
	case HW_BREAKPOINT_R:
		ret = trace_seq_printf(s, " R  ");
		break;
	case HW_BREAKPOINT_W:
		ret = trace_seq_printf(s, " W  ");
		break;
	case HW_BREAKPOINT_R | HW_BREAKPOINT_W:
		ret = trace_seq_printf(s, " RW ");
		break;
	default:
		return TRACE_TYPE_PARTIAL_LINE;
	}

	if (!ret)
		return TRACE_TYPE_PARTIAL_LINE;

	sprint_symbol(str, field->ip);
	ret = trace_seq_printf(s, "%s\n", str);
	if (!ret)
		return TRACE_TYPE_PARTIAL_LINE;

	return TRACE_TYPE_HANDLED;
}

struct tracer ksym_tracer __read_mostly =
{
	.name		= "ksym_tracer",
	.init		= ksym_trace_init,
	.reset		= ksym_trace_reset,
#ifdef CONFIG_FTRACE_SELFTEST
	.selftest	= trace_selftest_startup_ksym,
#endif
	.print_header   = ksym_trace_print_header,
	.print_line	= ksym_trace_output
};

#ifdef CONFIG_PROFILE_KSYM_TRACER
static int ksym_profile_show(struct seq_file *m, void *v)
{
	struct hlist_node *node;
	struct trace_ksym *entry;
	int access_type = 0;
	char fn_name[KSYM_NAME_LEN];

	seq_puts(m, "  Access Type ");
	seq_puts(m, "  Symbol                                       Counter\n");
	seq_puts(m, "  ----------- ");
	seq_puts(m, "  ------                                       -------\n");

	rcu_read_lock();
	hlist_for_each_entry_rcu(entry, node, &ksym_filter_head, ksym_hlist) {

		access_type = entry->attr.bp_type;

		switch (access_type) {
		case HW_BREAKPOINT_R:
			seq_puts(m, "  R           ");
			break;
		case HW_BREAKPOINT_W:
			seq_puts(m, "  W           ");
			break;
		case HW_BREAKPOINT_R | HW_BREAKPOINT_W:
			seq_puts(m, "  RW          ");
			break;
		default:
			seq_puts(m, "  NA          ");
		}

		if (lookup_symbol_name(entry->attr.bp_addr, fn_name) >= 0)
			seq_printf(m, "  %-36s", fn_name);
		else
			seq_printf(m, "  %-36s", "<NA>");
		seq_printf(m, " %15llu\n",
			   (unsigned long long)atomic64_read(&entry->counter));
	}
	rcu_read_unlock();

	return 0;
}

static int ksym_profile_open(struct inode *node, struct file *file)
{
	return single_open(file, ksym_profile_show, NULL);
}

static const struct file_operations ksym_profile_fops = {
	.open		= ksym_profile_open,
	.read		= seq_read,
	.llseek		= seq_lseek,
	.release	= single_release,
};
#endif /* CONFIG_PROFILE_KSYM_TRACER */

__init static int init_ksym_trace(void)
{
	struct dentry *d_tracer;

	d_tracer = tracing_init_dentry();

	trace_create_file("ksym_trace_filter", 0644, d_tracer,
			  NULL, &ksym_tracing_fops);

#ifdef CONFIG_PROFILE_KSYM_TRACER
	trace_create_file("ksym_profile", 0444, d_tracer,
			  NULL, &ksym_profile_fops);
#endif

	return register_tracer(&ksym_tracer);
}
device_initcall(init_ksym_trace);