blob: c188045c5f976cc35d20e6c72d108f934f0c27d6 [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0
/*
* trace_events_inject - trace event injection
*
* Copyright (C) 2019 Cong Wang <cwang@twitter.com>
*/
#include <linux/module.h>
#include <linux/ctype.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/rculist.h>
#include "trace.h"
static int
trace_inject_entry(struct trace_event_file *file, void *rec, int len)
{
struct trace_event_buffer fbuffer;
int written = 0;
void *entry;
rcu_read_lock_sched();
entry = trace_event_buffer_reserve(&fbuffer, file, len);
if (entry) {
memcpy(entry, rec, len);
written = len;
trace_event_buffer_commit(&fbuffer);
}
rcu_read_unlock_sched();
return written;
}
static int
parse_field(char *str, struct trace_event_call *call,
struct ftrace_event_field **pf, u64 *pv)
{
struct ftrace_event_field *field;
char *field_name;
int s, i = 0;
int len;
u64 val;
if (!str[i])
return 0;
/* First find the field to associate to */
while (isspace(str[i]))
i++;
s = i;
while (isalnum(str[i]) || str[i] == '_')
i++;
len = i - s;
if (!len)
return -EINVAL;
field_name = kmemdup_nul(str + s, len, GFP_KERNEL);
if (!field_name)
return -ENOMEM;
field = trace_find_event_field(call, field_name);
kfree(field_name);
if (!field)
return -ENOENT;
*pf = field;
while (isspace(str[i]))
i++;
if (str[i] != '=')
return -EINVAL;
i++;
while (isspace(str[i]))
i++;
s = i;
if (isdigit(str[i]) || str[i] == '-') {
char *num, c;
int ret;
/* Make sure the field is not a string */
if (is_string_field(field))
return -EINVAL;
if (str[i] == '-')
i++;
/* We allow 0xDEADBEEF */
while (isalnum(str[i]))
i++;
num = str + s;
c = str[i];
if (c != '\0' && !isspace(c))
return -EINVAL;
str[i] = '\0';
/* Make sure it is a value */
if (field->is_signed)
ret = kstrtoll(num, 0, &val);
else
ret = kstrtoull(num, 0, &val);
str[i] = c;
if (ret)
return ret;
*pv = val;
return i;
} else if (str[i] == '\'' || str[i] == '"') {
char q = str[i];
/* Make sure the field is OK for strings */
if (!is_string_field(field))
return -EINVAL;
for (i++; str[i]; i++) {
if (str[i] == '\\' && str[i + 1]) {
i++;
continue;
}
if (str[i] == q)
break;
}
if (!str[i])
return -EINVAL;
/* Skip quotes */
s++;
len = i - s;
if (len >= MAX_FILTER_STR_VAL)
return -EINVAL;
*pv = (unsigned long)(str + s);
str[i] = 0;
/* go past the last quote */
i++;
return i;
}
return -EINVAL;
}
static int trace_get_entry_size(struct trace_event_call *call)
{
struct ftrace_event_field *field;
struct list_head *head;
int size = 0;
head = trace_get_fields(call);
list_for_each_entry(field, head, link) {
if (field->size + field->offset > size)
size = field->size + field->offset;
}
return size;
}
static void *trace_alloc_entry(struct trace_event_call *call, int *size)
{
int entry_size = trace_get_entry_size(call);
struct ftrace_event_field *field;
struct list_head *head;
void *entry = NULL;
/* We need an extra '\0' at the end. */
entry = kzalloc(entry_size + 1, GFP_KERNEL);
if (!entry)
return NULL;
head = trace_get_fields(call);
list_for_each_entry(field, head, link) {
if (!is_string_field(field))
continue;
if (field->filter_type == FILTER_STATIC_STRING)
continue;
if (field->filter_type == FILTER_DYN_STRING) {
u32 *str_item;
int str_loc = entry_size & 0xffff;
str_item = (u32 *)(entry + field->offset);
*str_item = str_loc; /* string length is 0. */
} else {
char **paddr;
paddr = (char **)(entry + field->offset);
*paddr = "";
}
}
*size = entry_size + 1;
return entry;
}
#define INJECT_STRING "STATIC STRING CAN NOT BE INJECTED"
/* Caller is responsible to free the *pentry. */
static int parse_entry(char *str, struct trace_event_call *call, void **pentry)
{
struct ftrace_event_field *field;
void *entry = NULL;
int entry_size;
u64 val = 0;
int len;
entry = trace_alloc_entry(call, &entry_size);
*pentry = entry;
if (!entry)
return -ENOMEM;
tracing_generic_entry_update(entry, call->event.type,
tracing_gen_ctx());
while ((len = parse_field(str, call, &field, &val)) > 0) {
if (is_function_field(field))
return -EINVAL;
if (is_string_field(field)) {
char *addr = (char *)(unsigned long) val;
if (field->filter_type == FILTER_STATIC_STRING) {
strlcpy(entry + field->offset, addr, field->size);
} else if (field->filter_type == FILTER_DYN_STRING) {
int str_len = strlen(addr) + 1;
int str_loc = entry_size & 0xffff;
u32 *str_item;
entry_size += str_len;
*pentry = krealloc(entry, entry_size, GFP_KERNEL);
if (!*pentry) {
kfree(entry);
return -ENOMEM;
}
entry = *pentry;
strlcpy(entry + (entry_size - str_len), addr, str_len);
str_item = (u32 *)(entry + field->offset);
*str_item = (str_len << 16) | str_loc;
} else {
char **paddr;
paddr = (char **)(entry + field->offset);
*paddr = INJECT_STRING;
}
} else {
switch (field->size) {
case 1: {
u8 tmp = (u8) val;
memcpy(entry + field->offset, &tmp, 1);
break;
}
case 2: {
u16 tmp = (u16) val;
memcpy(entry + field->offset, &tmp, 2);
break;
}
case 4: {
u32 tmp = (u32) val;
memcpy(entry + field->offset, &tmp, 4);
break;
}
case 8:
memcpy(entry + field->offset, &val, 8);
break;
default:
return -EINVAL;
}
}
str += len;
}
if (len < 0)
return len;
return entry_size;
}
static ssize_t
event_inject_write(struct file *filp, const char __user *ubuf, size_t cnt,
loff_t *ppos)
{
struct trace_event_call *call;
struct trace_event_file *file;
int err = -ENODEV, size;
void *entry = NULL;
char *buf;
if (cnt >= PAGE_SIZE)
return -EINVAL;
buf = memdup_user_nul(ubuf, cnt);
if (IS_ERR(buf))
return PTR_ERR(buf);
strim(buf);
mutex_lock(&event_mutex);
file = event_file_data(filp);
if (file) {
call = file->event_call;
size = parse_entry(buf, call, &entry);
if (size < 0)
err = size;
else
err = trace_inject_entry(file, entry, size);
}
mutex_unlock(&event_mutex);
kfree(entry);
kfree(buf);
if (err < 0)
return err;
*ppos += err;
return cnt;
}
static ssize_t
event_inject_read(struct file *file, char __user *buf, size_t size,
loff_t *ppos)
{
return -EPERM;
}
const struct file_operations event_inject_fops = {
.open = tracing_open_generic,
.read = event_inject_read,
.write = event_inject_write,
};