| // 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 || | |
| field->filter_type == FILTER_RDYN_STRING) { | |
| u32 *str_item; | |
| int str_loc = entry_size & 0xffff; | |
| if (field->filter_type == FILTER_RDYN_STRING) | |
| str_loc -= field->offset + field->size; | |
| 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) { | |
| strscpy(entry + field->offset, addr, field->size); | |
| } else if (field->filter_type == FILTER_DYN_STRING || | |
| field->filter_type == FILTER_RDYN_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; | |
| strscpy(entry + (entry_size - str_len), addr, str_len); | |
| str_item = (u32 *)(entry + field->offset); | |
| if (field->filter_type == FILTER_RDYN_STRING) | |
| str_loc -= field->offset + field->size; | |
| *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_file(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_file_tr, | |
| .read = event_inject_read, | |
| .write = event_inject_write, | |
| .release = tracing_release_file_tr, | |
| }; |