Commit message: tracing: Have error path in predicate_parse() free its allocated memory

In predicate_parse, there is an error path that is not going to
out_free instead it returns directly which leads to a memory leak.

Link: http://lkml.kernel.org/r/20190920225800.3870-1-navid.emamdoost@gmail.com

Signed-off-by: Navid Emamdoost <navid.emamdoost@gmail.com>
Signed-off-by: Steven Rostedt (VMware) <rostedt@goodmis.org>
CVE: CVE-2019-19072
CWE: CWE-284
Vulnerability description: A memory leak in the predicate_parse() function in kernel/trace/trace_events_filter.c in the Linux kernel through 5.3.11 allows attackers to cause a denial of service (memory consumption), aka CID-96c5c6e6a5b6.

Project: linux
File: trace_events_filter.c

int apply_event_filter(struct trace_event_file *file, char *filter_string)
{
	struct trace_event_call *call = file->event_call;
	struct event_filter *filter = NULL;
	int err;

	if (!strcmp(strstrip(filter_string), "0")) {
		filter_disable(file);
		filter = event_filter(file);

		if (!filter)
			return 0;

		event_clear_filter(file);

		/* Make sure the filter is not being used */
		tracepoint_synchronize_unregister();
		__free_filter(filter);

		return 0;
	}

	err = create_filter(file->tr, call, filter_string, true, &filter);

	/*
	 * Always swap the call filter with the new filter
	 * even if there was an error. If there was an error
	 * in the filter, we disable the filter and show the error
	 * string
	 */
	if (filter) {
		struct event_filter *tmp;

		tmp = event_filter(file);
		if (!err)
			event_set_filtered_flag(file);
		else
			filter_disable(file);

		event_set_filter(file, filter);

		if (tmp) {
			/* Make sure the call is done with the filter */
			tracepoint_synchronize_unregister();
			__free_filter(tmp);
		}
	}

	return err;
}

Project: linux
File: trace_events_filter.c

static int process_system_preds(struct trace_subsystem_dir *dir,
				struct trace_array *tr,
				struct filter_parse_error *pe,
				char *filter_string)
{
	struct trace_event_file *file;
	struct filter_list *filter_item;
	struct event_filter *filter = NULL;
	struct filter_list *tmp;
	LIST_HEAD(filter_list);
	bool fail = true;
	int err;

	list_for_each_entry(file, &tr->events, list) {

		if (file->system != dir)
			continue;

		filter = kzalloc(sizeof(*filter), GFP_KERNEL);
		if (!filter)
			goto fail_mem;

		filter->filter_string = kstrdup(filter_string, GFP_KERNEL);
		if (!filter->filter_string)
			goto fail_mem;

		err = process_preds(file->event_call, filter_string, filter, pe);
		if (err) {
			filter_disable(file);
			parse_error(pe, FILT_ERR_BAD_SUBSYS_FILTER, 0);
			append_filter_err(tr, pe, filter);
		} else
			event_set_filtered_flag(file);


		filter_item = kzalloc(sizeof(*filter_item), GFP_KERNEL);
		if (!filter_item)
			goto fail_mem;

		list_add_tail(&filter_item->list, &filter_list);
		/*
		 * Regardless of if this returned an error, we still
		 * replace the filter for the call.
		 */
		filter_item->filter = event_filter(file);
		event_set_filter(file, filter);
		filter = NULL;

		fail = false;
	}

	if (fail)
		goto fail;

	/*
	 * The calls can still be using the old filters.
	 * Do a synchronize_rcu() and to ensure all calls are
	 * done with them before we free them.
	 */
	tracepoint_synchronize_unregister();
	list_for_each_entry_safe(filter_item, tmp, &filter_list, list) {
		__free_filter(filter_item->filter);
		list_del(&filter_item->list);
		kfree(filter_item);
	}
	return 0;
 fail:
	/* No call succeeded */
	list_for_each_entry_safe(filter_item, tmp, &filter_list, list) {
		list_del(&filter_item->list);
		kfree(filter_item);
	}
	parse_error(pe, FILT_ERR_BAD_SUBSYS_FILTER, 0);
	return -EINVAL;
 fail_mem:
	kfree(filter);
	/* If any call succeeded, we still need to sync */
	if (!fail)
		tracepoint_synchronize_unregister();
	list_for_each_entry_safe(filter_item, tmp, &filter_list, list) {
		__free_filter(filter_item->filter);
		list_del(&filter_item->list);
		kfree(filter_item);
	}
	return -ENOMEM;
}

Project: linux
File: trace_events_filter.c

int apply_subsystem_event_filter(struct trace_subsystem_dir *dir,
				 char *filter_string)
{
	struct event_subsystem *system = dir->subsystem;
	struct trace_array *tr = dir->tr;
	struct event_filter *filter = NULL;
	int err = 0;

	mutex_lock(&event_mutex);

	/* Make sure the system still has events */
	if (!dir->nr_events) {
		err = -ENODEV;
		goto out_unlock;
	}

	if (!strcmp(strstrip(filter_string), "0")) {
		filter_free_subsystem_preds(dir, tr);
		remove_filter_string(system->filter);
		filter = system->filter;
		system->filter = NULL;
		/* Ensure all filters are no longer used */
		tracepoint_synchronize_unregister();
		filter_free_subsystem_filters(dir, tr);
		__free_filter(filter);
		goto out_unlock;
	}

	err = create_system_filter(dir, tr, filter_string, &filter);
	if (filter) {
		/*
		 * No event actually uses the system filter
		 * we can free it without synchronize_rcu().
		 */
		__free_filter(system->filter);
		system->filter = filter;
	}
out_unlock:
	mutex_unlock(&event_mutex);

	return err;
}

Project: linux
File: trace_events_filter.c

int ftrace_profile_set_filter(struct perf_event *event, int event_id,
			      char *filter_str)
{
	int err;
	struct event_filter *filter = NULL;
	struct trace_event_call *call;

	mutex_lock(&event_mutex);

	call = event->tp_event;

	err = -EINVAL;
	if (!call)
		goto out_unlock;

	err = -EEXIST;
	if (event->filter)
		goto out_unlock;

	err = create_filter(NULL, call, filter_str, false, &filter);
	if (err)
		goto free_filter;

	if (ftrace_event_is_function(call))
		err = ftrace_function_set_filter(event, filter);
	else
		event->filter = filter;

free_filter:
	if (err || ftrace_event_is_function(call))
		__free_filter(filter);

out_unlock:
	mutex_unlock(&event_mutex);

	return err;
}

Project: linux
File: trace_events_filter.c

static int create_filter(struct trace_array *tr,
			 struct trace_event_call *call,
			 char *filter_string, bool set_str,
			 struct event_filter **filterp)
{
	struct filter_parse_error *pe = NULL;
	int err;

	/* filterp must point to NULL */
	if (WARN_ON(*filterp))
		*filterp = NULL;

	err = create_filter_start(filter_string, set_str, &pe, filterp);
	if (err)
		return err;

	err = process_preds(call, filter_string, *filterp, pe);
	if (err && set_str)
		append_filter_err(tr, pe, *filterp);
	create_filter_finish(pe);

	return err;
}

Project: linux
File: trace_events_filter.c

predicate_parse(const char *str, int nr_parens, int nr_preds,
		parse_pred_fn parse_pred, void *data,
		struct filter_parse_error *pe)
{
	struct prog_entry *prog_stack;
	struct prog_entry *prog;
	const char *ptr = str;
	char *inverts = NULL;
	int *op_stack;
	int *top;
	int invert = 0;
	int ret = -ENOMEM;
	int len;
	int N = 0;
	int i;

	nr_preds += 2; /* For TRUE and FALSE */

	op_stack = kmalloc_array(nr_parens, sizeof(*op_stack), GFP_KERNEL);
	if (!op_stack)
		return ERR_PTR(-ENOMEM);
	prog_stack = kcalloc(nr_preds, sizeof(*prog_stack), GFP_KERNEL);
	if (!prog_stack) {
		parse_error(pe, -ENOMEM, 0);
		goto out_free;
	}
	inverts = kmalloc_array(nr_preds, sizeof(*inverts), GFP_KERNEL);
	if (!inverts) {
		parse_error(pe, -ENOMEM, 0);
		goto out_free;
	}

	top = op_stack;
	prog = prog_stack;
	*top = 0;

	/* First pass */
	while (*ptr) {						/* #1 */
		const char *next = ptr++;

		if (isspace(*next))
			continue;

		switch (*next) {
		case '(':					/* #2 */
			if (top - op_stack > nr_parens) {
				ret = -EINVAL;
				goto out_free;
			}
			*(++top) = invert;
			continue;
		case '!':					/* #3 */
			if (!is_not(next))
				break;
			invert = !invert;
			continue;
		}

		if (N >= nr_preds) {
			parse_error(pe, FILT_ERR_TOO_MANY_PREDS, next - str);
			goto out_free;
		}

		inverts[N] = invert;				/* #4 */
		prog[N].target = N-1;

		len = parse_pred(next, data, ptr - str, pe, &prog[N].pred);
		if (len < 0) {
			ret = len;
			goto out_free;
		}
		ptr = next + len;

		N++;

		ret = -1;
		while (1) {					/* #5 */
			next = ptr++;
			if (isspace(*next))
				continue;

			switch (*next) {
			case ')':
			case '\0':
				break;
			case '&':
			case '|':
				/* accepting only "&&" or "||" */
				if (next[1] == next[0]) {
					ptr++;
					break;
				}
				/* fall through */
			default:
				parse_error(pe, FILT_ERR_TOO_MANY_PREDS,
					    next - str);
				goto out_free;
			}

			invert = *top & INVERT;

			if (*top & PROCESS_AND) {		/* #7 */
				update_preds(prog, N - 1, invert);
				*top &= ~PROCESS_AND;
			}
			if (*next == '&') {			/* #8 */
				*top |= PROCESS_AND;
				break;
			}
			if (*top & PROCESS_OR) {		/* #9 */
				update_preds(prog, N - 1, !invert);
				*top &= ~PROCESS_OR;
			}
			if (*next == '|') {			/* #10 */
				*top |= PROCESS_OR;
				break;
			}
			if (!*next)				/* #11 */
				goto out;

			if (top == op_stack) {
				ret = -1;
				/* Too few '(' */
				parse_error(pe, FILT_ERR_TOO_MANY_CLOSE, ptr - str);
				goto out_free;
			}
			top--;					/* #12 */
		}
	}
 out:
	if (top != op_stack) {
		/* Too many '(' */
		parse_error(pe, FILT_ERR_TOO_MANY_OPEN, ptr - str);
		goto out_free;
	}

	if (!N) {
		/* No program? */
		ret = -EINVAL;
		parse_error(pe, FILT_ERR_NO_FILTER, ptr - str);
		goto out_free;
	}

	prog[N].pred = NULL;					/* #13 */
	prog[N].target = 1;		/* TRUE */
	prog[N+1].pred = NULL;
	prog[N+1].target = 0;		/* FALSE */
	prog[N-1].target = N;
	prog[N-1].when_to_branch = false;

	/* Second Pass */
	for (i = N-1 ; i--; ) {
		int target = prog[i].target;
		if (prog[i].when_to_branch == prog[target].when_to_branch)
			prog[i].target = prog[target].target;
	}

	/* Third Pass */
	for (i = 0; i < N; i++) {
		invert = inverts[i] ^ prog[i].when_to_branch;
		prog[i].when_to_branch = invert;
		/* Make sure the program always moves forward */
		if (WARN_ON(prog[i].target <= i)) {
			ret = -EINVAL;
			goto out_free;
		}
	}

	kfree(op_stack);
	kfree(inverts);
	return prog;
out_free:
	kfree(op_stack);
	kfree(inverts);
	if (prog_stack) {
		for (i = 0; prog_stack[i].pred; i++)
			kfree(prog_stack[i].pred);
		kfree(prog_stack);
	}
	return ERR_PTR(ret);
}

Project: linux
File: trace_events_filter.c

static void filter_build_regex(struct filter_pred *pred)
{
	struct regex *r = &pred->regex;
	char *search;
	enum regex_type type = MATCH_FULL;

	if (pred->op == OP_GLOB) {
		type = filter_parse_regex(r->pattern, r->len, &search, &pred->not);
		r->len = strlen(search);
		memmove(r->pattern, search, r->len+1);
	}

	switch (type) {
	/* MATCH_INDEX should not happen, but if it does, match full */
	case MATCH_INDEX:
	case MATCH_FULL:
		r->match = regex_match_full;
		break;
	case MATCH_FRONT_ONLY:
		r->match = regex_match_front;
		break;
	case MATCH_MIDDLE_ONLY:
		r->match = regex_match_middle;
		break;
	case MATCH_END_ONLY:
		r->match = regex_match_end;
		break;
	case MATCH_GLOB:
		r->match = regex_match_glob;
		break;
	}
}

Project: linux
File: trace_events_filter.c

enum regex_type filter_parse_regex(char *buff, int len, char **search, int *not)
{
	int type = MATCH_FULL;
	int i;

	if (buff[0] == '!') {
		*not = 1;
		buff++;
		len--;
	} else
		*not = 0;

	*search = buff;

	if (isdigit(buff[0]))
		return MATCH_INDEX;

	for (i = 0; i < len; i++) {
		if (buff[i] == '*') {
			if (!i) {
				type = MATCH_END_ONLY;
			} else if (i == len - 1) {
				if (type == MATCH_END_ONLY)
					type = MATCH_MIDDLE_ONLY;
				else
					type = MATCH_FRONT_ONLY;
				buff[i] = 0;
				break;
			} else {	/* pattern continues, use full glob */
				return MATCH_GLOB;
			}
		} else if (strchr("[?\\", buff[i])) {
			return MATCH_GLOB;
		}
	}
	if (buff[0] == '*')
		*search = buff + 1;

	return type;
}

Project: linux
File: trace_events_filter.c

static int parse_pred(const char *str, void *data,
		      int pos, struct filter_parse_error *pe,
		      struct filter_pred **pred_ptr)
{
	struct trace_event_call *call = data;
	struct ftrace_event_field *field;
	struct filter_pred *pred = NULL;
	char num_buf[24];	/* Big enough to hold an address */
	char *field_name;
	char q;
	u64 val;
	int len;
	int ret;
	int op;
	int s;
	int i = 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 -1;

	field_name = kmemdup_nul(str + s, len, GFP_KERNEL);
	if (!field_name)
		return -ENOMEM;

	/* Make sure that the field exists */

	field = trace_find_event_field(call, field_name);
	kfree(field_name);
	if (!field) {
		parse_error(pe, FILT_ERR_FIELD_NOT_FOUND, pos + i);
		return -EINVAL;
	}

	while (isspace(str[i]))
		i++;

	/* Make sure this op is supported */
	for (op = 0; ops[op]; op++) {
		/* This is why '<=' must come before '<' in ops[] */
		if (strncmp(str + i, ops[op], strlen(ops[op])) == 0)
			break;
	}

	if (!ops[op]) {
		parse_error(pe, FILT_ERR_INVALID_OP, pos + i);
		goto err_free;
	}

	i += strlen(ops[op]);

	while (isspace(str[i]))
		i++;

	s = i;

	pred = kzalloc(sizeof(*pred), GFP_KERNEL);
	if (!pred)
		return -ENOMEM;

	pred->field = field;
	pred->offset = field->offset;
	pred->op = op;

	if (ftrace_event_is_function(call)) {
		/*
		 * Perf does things different with function events.
		 * It only allows an "ip" field, and expects a string.
		 * But the string does not need to be surrounded by quotes.
		 * If it is a string, the assigned function as a nop,
		 * (perf doesn't use it) and grab everything.
		 */
		if (strcmp(field->name, "ip") != 0) {
			parse_error(pe, FILT_ERR_IP_FIELD_ONLY, pos + i);
			goto err_free;
		}
		pred->fn = filter_pred_none;

		/*
		 * Quotes are not required, but if they exist then we need
		 * to read them till we hit a matching one.
		 */
		if (str[i] == '\'' || str[i] == '"')
			q = str[i];
		else
			q = 0;

		for (i++; str[i]; i++) {
			if (q && str[i] == q)
				break;
			if (!q && (str[i] == ')' || str[i] == '&' ||
				   str[i] == '|'))
				break;
		}
		/* Skip quotes */
		if (q)
			s++;
		len = i - s;
		if (len >= MAX_FILTER_STR_VAL) {
			parse_error(pe, FILT_ERR_OPERAND_TOO_LONG, pos + i);
			goto err_free;
		}

		pred->regex.len = len;
		strncpy(pred->regex.pattern, str + s, len);
		pred->regex.pattern[len] = 0;

	/* This is either a string, or an integer */
	} else if (str[i] == '\'' || str[i] == '"') {
		char q = str[i];

		/* Make sure the op is OK for strings */
		switch (op) {
		case OP_NE:
			pred->not = 1;
			/* Fall through */
		case OP_GLOB:
		case OP_EQ:
			break;
		default:
			parse_error(pe, FILT_ERR_ILLEGAL_FIELD_OP, pos + i);
			goto err_free;
		}

		/* Make sure the field is OK for strings */
		if (!is_string_field(field)) {
			parse_error(pe, FILT_ERR_EXPECT_DIGIT, pos + i);
			goto err_free;
		}

		for (i++; str[i]; i++) {
			if (str[i] == q)
				break;
		}
		if (!str[i]) {
			parse_error(pe, FILT_ERR_MISSING_QUOTE, pos + i);
			goto err_free;
		}

		/* Skip quotes */
		s++;
		len = i - s;
		if (len >= MAX_FILTER_STR_VAL) {
			parse_error(pe, FILT_ERR_OPERAND_TOO_LONG, pos + i);
			goto err_free;
		}

		pred->regex.len = len;
		strncpy(pred->regex.pattern, str + s, len);
		pred->regex.pattern[len] = 0;

		filter_build_regex(pred);

		if (field->filter_type == FILTER_COMM) {
			pred->fn = filter_pred_comm;

		} else if (field->filter_type == FILTER_STATIC_STRING) {
			pred->fn = filter_pred_string;
			pred->regex.field_len = field->size;

		} else if (field->filter_type == FILTER_DYN_STRING)
			pred->fn = filter_pred_strloc;
		else
			pred->fn = filter_pred_pchar;
		/* go past the last quote */
		i++;

	} else if (isdigit(str[i]) || str[i] == '-') {

		/* Make sure the field is not a string */
		if (is_string_field(field)) {
			parse_error(pe, FILT_ERR_EXPECT_STRING, pos + i);
			goto err_free;
		}

		if (op == OP_GLOB) {
			parse_error(pe, FILT_ERR_ILLEGAL_FIELD_OP, pos + i);
			goto err_free;
		}

		if (str[i] == '-')
			i++;

		/* We allow 0xDEADBEEF */
		while (isalnum(str[i]))
			i++;

		len = i - s;
		/* 0xfeedfacedeadbeef is 18 chars max */
		if (len >= sizeof(num_buf)) {
			parse_error(pe, FILT_ERR_OPERAND_TOO_LONG, pos + i);
			goto err_free;
		}

		strncpy(num_buf, str + s, len);
		num_buf[len] = 0;

		/* Make sure it is a value */
		if (field->is_signed)
			ret = kstrtoll(num_buf, 0, &val);
		else
			ret = kstrtoull(num_buf, 0, &val);
		if (ret) {
			parse_error(pe, FILT_ERR_ILLEGAL_INTVAL, pos + s);
			goto err_free;
		}

		pred->val = val;

		if (field->filter_type == FILTER_CPU)
			pred->fn = filter_pred_cpu;
		else {
			pred->fn = select_comparison_fn(pred->op, field->size,
							field->is_signed);
			if (pred->op == OP_NE)
				pred->not = 1;
		}

	} else {
		parse_error(pe, FILT_ERR_INVALID_VALUE, pos + i);
		goto err_free;
	}

	*pred_ptr = pred;
	return i;

err_free:
	kfree(pred);
	return -EINVAL;
}

Project: linux
File: trace_events_filter.c

static int create_system_filter(struct trace_subsystem_dir *dir,
				struct trace_array *tr,
				char *filter_str, struct event_filter **filterp)
{
	struct filter_parse_error *pe = NULL;
	int err;

	err = create_filter_start(filter_str, true, &pe, filterp);
	if (!err) {
		err = process_system_preds(dir, tr, pe, filter_str);
		if (!err) {
			/* System filters just show a default message */
			kfree((*filterp)->filter_string);
			(*filterp)->filter_string = NULL;
		} else {
			append_filter_err(tr, pe, *filterp);
		}
	}
	create_filter_finish(pe);

	return err;
}

Project: linux
File: trace_events_filter.c

static void append_filter_err(struct trace_array *tr,
			      struct filter_parse_error *pe,
			      struct event_filter *filter)
{
	struct trace_seq *s;
	int pos = pe->lasterr_pos;
	char *buf;
	int len;

	if (WARN_ON(!filter->filter_string))
		return;

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

	len = strlen(filter->filter_string);
	if (pos > len)
		pos = len;

	/* indexing is off by one */
	if (pos)
		pos++;

	trace_seq_puts(s, filter->filter_string);
	if (pe->lasterr > 0) {
		trace_seq_printf(s, "\n%*s", pos, "^");
		trace_seq_printf(s, "\nparse_error: %s\n", err_text[pe->lasterr]);
		tracing_log_err(tr, "event filter parse error",
				filter->filter_string, err_text,
				pe->lasterr, pe->lasterr_pos);
	} else {
		trace_seq_printf(s, "\nError: (%d)\n", pe->lasterr);
		tracing_log_err(tr, "event filter parse error",
				filter->filter_string, err_text,
				FILT_ERR_ERRNO, 0);
	}
	trace_seq_putc(s, 0);
	buf = kmemdup_nul(s->buffer, s->seq.len, GFP_KERNEL);
	if (buf) {
		kfree(filter->filter_string);
		filter->filter_string = buf;
	}
	kfree(s);
}

Project: linux
File: trace_events_filter.c

int filter_match_preds(struct event_filter *filter, void *rec)
{
	struct prog_entry *prog;
	int i;

	/* no filter is considered a match */
	if (!filter)
		return 1;

	/* Protected by either SRCU(tracepoint_srcu) or preempt_disable */
	prog = rcu_dereference_raw(filter->prog);
	if (!prog)
		return 1;

	for (i = 0; prog[i].pred; i++) {
		struct filter_pred *pred = prog[i].pred;
		int match = pred->fn(pred, rec);
		if (match == prog[i].when_to_branch)
			i = prog[i].target;
	}
	return prog[i].target;
}

Project: linux
File: trace_events_filter.c

static __init int ftrace_test_event_filter(void)
{
	int i;

	printk(KERN_INFO "Testing ftrace filter: ");

	for (i = 0; i < DATA_CNT; i++) {
		struct event_filter *filter = NULL;
		struct test_filter_data_t *d = &test_filter_data[i];
		int err;

		err = create_filter(NULL, &event_ftrace_test_filter,
				    d->filter, false, &filter);
		if (err) {
			printk(KERN_INFO
			       "Failed to get filter for '%s', err %d\n",
			       d->filter, err);
			__free_filter(filter);
			break;
		}

		/* Needed to dereference filter->prog */
		mutex_lock(&event_mutex);
		/*
		 * The preemption disabling is not really needed for self
		 * tests, but the rcu dereference will complain without it.
		 */
		preempt_disable();
		if (*d->not_visited)
			update_pred_fn(filter, d->not_visited);

		test_pred_visited = 0;
		err = filter_match_preds(filter, &d->rec);
		preempt_enable();

		mutex_unlock(&event_mutex);

		__free_filter(filter);

		if (test_pred_visited) {
			printk(KERN_INFO
			       "Failed, unwanted pred visited for filter %s\n",
			       d->filter);
			break;
		}

		if (err != d->match) {
			printk(KERN_INFO
			       "Failed to match filter '%s', expected %d\n",
			       d->filter, d->match);
			break;
		}
	}

	if (i == DATA_CNT)
		printk(KERN_CONT "OK\n");

	return 0;
}

Project: linux
File: trace_events_filter.c

int create_event_filter(struct trace_array *tr,
			struct trace_event_call *call,
			char *filter_str, bool set_str,
			struct event_filter **filterp)
{
	return create_filter(tr, call, filter_str, set_str, filterp);
}

Project: linux
File: trace_events_filter.c

int filter_assign_type(const char *type)
{
	if (strstr(type, "__data_loc") && strstr(type, "char"))
		return FILTER_DYN_STRING;

	if (strchr(type, '[') && strstr(type, "char"))
		return FILTER_STATIC_STRING;

	if (strcmp(type, "char *") == 0 || strcmp(type, "const char *") == 0)
		return FILTER_PTR_STRING;

	return FILTER_OTHER;
}