tracing: Check "%s" dereference via the field and not the TP_printk format

The TP_printk() portion of a trace event is executed at the time a event
is read from the trace. This can happen seconds, minutes, hours, days,
months, years possibly later since the event was recorded. If the print
format contains a dereference to a string via "%s", and that string was
allocated, there's a chance that string could be freed before it is read
by the trace file.

To protect against such bugs, there are two functions that verify the
event. The first one is test_event_printk(), which is called when the
event is created. It reads the TP_printk() format as well as its arguments
to make sure nothing may be dereferencing a pointer that was not copied
into the ring buffer along with the event. If it is, it will trigger a
WARN_ON().

For strings that use "%s", it is not so easy. The string may not reside in
the ring buffer but may still be valid. Strings that are static and part
of the kernel proper which will not be freed for the life of the running
system, are safe to dereference. But to know if it is a pointer to a
static string or to something on the heap can not be determined until the
event is triggered.

This brings us to the second function that tests for the bad dereferencing
of strings, trace_check_vprintf(). It would walk through the printf format
looking for "%s", and when it finds it, it would validate that the pointer
is safe to read. If not, it would produces a WARN_ON() as well and write
into the ring buffer "[UNSAFE-MEMORY]".

The problem with this is how it used va_list to have vsnprintf() handle
all the cases that it didn't need to check. Instead of re-implementing
vsnprintf(), it would make a copy of the format up to the %s part, and
call vsnprintf() with the current va_list ap variable, where the ap would
then be ready to point at the string in question.

For architectures that passed va_list by reference this was possible. For
architectures that passed it by copy it was not. A test_can_verify()
function was used to differentiate between the two, and if it wasn't
possible, it would disable it.

Even for architectures where this was feasible, it was a stretch to rely
on such a method that is undocumented, and could cause issues later on
with new optimizations of the compiler.

Instead, the first function test_event_printk() was updated to look at
"%s" as well. If the "%s" argument is a pointer outside the event in the
ring buffer, it would find the field type of the event that is the problem
and mark the structure with a new flag called "needs_test". The event
itself will be marked by TRACE_EVENT_FL_TEST_STR to let it be known that
this event has a field that needs to be verified before the event can be
printed using the printf format.

When the event fields are created from the field type structure, the
fields would copy the field type's "needs_test" value.

Finally, before being printed, a new function ignore_event() is called
which will check if the event has the TEST_STR flag set (if not, it
returns false). If the flag is set, it then iterates through the events
fields looking for the ones that have the "needs_test" flag set.

Then it uses the offset field from the field structure to find the pointer
in the ring buffer event. It runs the tests to make sure that pointer is
safe to print and if not, it triggers the WARN_ON() and also adds to the
trace output that the event in question has an unsafe memory access.

The ignore_event() makes the trace_check_vprintf() obsolete so it is
removed.

Link: https://lore.kernel.org/all/CAHk-=wh3uOnqnZPpR0PeLZZtyWbZLboZ7cHLCKRWsocvs9Y7hQ@mail.gmail.com/

Cc: stable@vger.kernel.org
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mark Rutland <mark.rutland@arm.com>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Andrew Morton <akpm@linux-foundation.org>
Cc: Al Viro <viro@ZenIV.linux.org.uk>
Cc: Linus Torvalds <torvalds@linux-foundation.org>
Link: https://lore.kernel.org/20241217024720.848621576@goodmis.org
Fixes: 5013f454a3 ("tracing: Add check of trace event print fmts for dereferencing pointers")
Signed-off-by: Steven Rostedt (Google) <rostedt@goodmis.org>
This commit is contained in:
Steven Rostedt 2024-12-16 21:41:22 -05:00 committed by Steven Rostedt (Google)
parent 65a25d9f7a
commit afd2627f72
5 changed files with 88 additions and 217 deletions

View File

@ -273,7 +273,8 @@ struct trace_event_fields {
const char *name; const char *name;
const int size; const int size;
const int align; const int align;
const int is_signed; const unsigned int is_signed:1;
unsigned int needs_test:1;
const int filter_type; const int filter_type;
const int len; const int len;
}; };
@ -324,6 +325,7 @@ enum {
TRACE_EVENT_FL_EPROBE_BIT, TRACE_EVENT_FL_EPROBE_BIT,
TRACE_EVENT_FL_FPROBE_BIT, TRACE_EVENT_FL_FPROBE_BIT,
TRACE_EVENT_FL_CUSTOM_BIT, TRACE_EVENT_FL_CUSTOM_BIT,
TRACE_EVENT_FL_TEST_STR_BIT,
}; };
/* /*
@ -340,6 +342,7 @@ enum {
* CUSTOM - Event is a custom event (to be attached to an exsiting tracepoint) * CUSTOM - Event is a custom event (to be attached to an exsiting tracepoint)
* This is set when the custom event has not been attached * This is set when the custom event has not been attached
* to a tracepoint yet, then it is cleared when it is. * to a tracepoint yet, then it is cleared when it is.
* TEST_STR - The event has a "%s" that points to a string outside the event
*/ */
enum { enum {
TRACE_EVENT_FL_CAP_ANY = (1 << TRACE_EVENT_FL_CAP_ANY_BIT), TRACE_EVENT_FL_CAP_ANY = (1 << TRACE_EVENT_FL_CAP_ANY_BIT),
@ -352,6 +355,7 @@ enum {
TRACE_EVENT_FL_EPROBE = (1 << TRACE_EVENT_FL_EPROBE_BIT), TRACE_EVENT_FL_EPROBE = (1 << TRACE_EVENT_FL_EPROBE_BIT),
TRACE_EVENT_FL_FPROBE = (1 << TRACE_EVENT_FL_FPROBE_BIT), TRACE_EVENT_FL_FPROBE = (1 << TRACE_EVENT_FL_FPROBE_BIT),
TRACE_EVENT_FL_CUSTOM = (1 << TRACE_EVENT_FL_CUSTOM_BIT), TRACE_EVENT_FL_CUSTOM = (1 << TRACE_EVENT_FL_CUSTOM_BIT),
TRACE_EVENT_FL_TEST_STR = (1 << TRACE_EVENT_FL_TEST_STR_BIT),
}; };
#define TRACE_EVENT_FL_UKPROBE (TRACE_EVENT_FL_KPROBE | TRACE_EVENT_FL_UPROBE) #define TRACE_EVENT_FL_UKPROBE (TRACE_EVENT_FL_KPROBE | TRACE_EVENT_FL_UPROBE)

View File

@ -3611,17 +3611,12 @@ char *trace_iter_expand_format(struct trace_iterator *iter)
} }
/* Returns true if the string is safe to dereference from an event */ /* Returns true if the string is safe to dereference from an event */
static bool trace_safe_str(struct trace_iterator *iter, const char *str, static bool trace_safe_str(struct trace_iterator *iter, const char *str)
bool star, int len)
{ {
unsigned long addr = (unsigned long)str; unsigned long addr = (unsigned long)str;
struct trace_event *trace_event; struct trace_event *trace_event;
struct trace_event_call *event; struct trace_event_call *event;
/* Ignore strings with no length */
if (star && !len)
return true;
/* OK if part of the event data */ /* OK if part of the event data */
if ((addr >= (unsigned long)iter->ent) && if ((addr >= (unsigned long)iter->ent) &&
(addr < (unsigned long)iter->ent + iter->ent_size)) (addr < (unsigned long)iter->ent + iter->ent_size))
@ -3661,181 +3656,69 @@ static bool trace_safe_str(struct trace_iterator *iter, const char *str,
return false; return false;
} }
static DEFINE_STATIC_KEY_FALSE(trace_no_verify);
static int test_can_verify_check(const char *fmt, ...)
{
char buf[16];
va_list ap;
int ret;
/*
* The verifier is dependent on vsnprintf() modifies the va_list
* passed to it, where it is sent as a reference. Some architectures
* (like x86_32) passes it by value, which means that vsnprintf()
* does not modify the va_list passed to it, and the verifier
* would then need to be able to understand all the values that
* vsnprintf can use. If it is passed by value, then the verifier
* is disabled.
*/
va_start(ap, fmt);
vsnprintf(buf, 16, "%d", ap);
ret = va_arg(ap, int);
va_end(ap);
return ret;
}
static void test_can_verify(void)
{
if (!test_can_verify_check("%d %d", 0, 1)) {
pr_info("trace event string verifier disabled\n");
static_branch_inc(&trace_no_verify);
}
}
/** /**
* trace_check_vprintf - Check dereferenced strings while writing to the seq buffer * ignore_event - Check dereferenced fields while writing to the seq buffer
* @iter: The iterator that holds the seq buffer and the event being printed * @iter: The iterator that holds the seq buffer and the event being printed
* @fmt: The format used to print the event
* @ap: The va_list holding the data to print from @fmt.
* *
* This writes the data into the @iter->seq buffer using the data from * At boot up, test_event_printk() will flag any event that dereferences
* @fmt and @ap. If the format has a %s, then the source of the string * a string with "%s" that does exist in the ring buffer. It may still
* is examined to make sure it is safe to print, otherwise it will * be valid, as the string may point to a static string in the kernel
* warn and print "[UNSAFE MEMORY]" in place of the dereferenced string * rodata that never gets freed. But if the string pointer is pointing
* pointer. * to something that was allocated, there's a chance that it can be freed
* by the time the user reads the trace. This would cause a bad memory
* access by the kernel and possibly crash the system.
*
* This function will check if the event has any fields flagged as needing
* to be checked at runtime and perform those checks.
*
* If it is found that a field is unsafe, it will write into the @iter->seq
* a message stating what was found to be unsafe.
*
* @return: true if the event is unsafe and should be ignored,
* false otherwise.
*/ */
void trace_check_vprintf(struct trace_iterator *iter, const char *fmt, bool ignore_event(struct trace_iterator *iter)
va_list ap)
{ {
long text_delta = 0; struct ftrace_event_field *field;
long data_delta = 0; struct trace_event *trace_event;
const char *p = fmt; struct trace_event_call *event;
const char *str; struct list_head *head;
bool good; struct trace_seq *seq;
int i, j; const void *ptr;
if (WARN_ON_ONCE(!fmt)) trace_event = ftrace_find_event(iter->ent->type);
return;
if (static_branch_unlikely(&trace_no_verify)) seq = &iter->seq;
goto print;
/* if (!trace_event) {
* When the kernel is booted with the tp_printk command line trace_seq_printf(seq, "EVENT ID %d NOT FOUND?\n", iter->ent->type);
* parameter, trace events go directly through to printk(). return true;
* It also is checked by this function, but it does not
* have an associated trace_array (tr) for it.
*/
if (iter->tr) {
text_delta = iter->tr->text_delta;
data_delta = iter->tr->data_delta;
} }
/* Don't bother checking when doing a ftrace_dump() */ event = container_of(trace_event, struct trace_event_call, event);
if (iter->fmt == static_fmt_buf) if (!(event->flags & TRACE_EVENT_FL_TEST_STR))
goto print; return false;
while (*p) { head = trace_get_fields(event);
bool star = false; if (!head) {
int len = 0; trace_seq_printf(seq, "FIELDS FOR EVENT '%s' NOT FOUND?\n",
trace_event_name(event));
return true;
}
j = 0; /* Offsets are from the iter->ent that points to the raw event */
ptr = iter->ent;
/* list_for_each_entry(field, head, link) {
* We only care about %s and variants const char *str;
* as well as %p[sS] if delta is non-zero bool good;
*/
for (i = 0; p[i]; i++) {
if (i + 1 >= iter->fmt_size) {
/*
* If we can't expand the copy buffer,
* just print it.
*/
if (!trace_iter_expand_format(iter))
goto print;
}
if (p[i] == '\\' && p[i+1]) { if (!field->needs_test)
i++;
continue;
}
if (p[i] == '%') {
/* Need to test cases like %08.*s */
for (j = 1; p[i+j]; j++) {
if (isdigit(p[i+j]) ||
p[i+j] == '.')
continue;
if (p[i+j] == '*') {
star = true;
continue;
}
break;
}
if (p[i+j] == 's')
break;
if (text_delta && p[i+1] == 'p' &&
((p[i+2] == 's' || p[i+2] == 'S')))
break;
star = false;
}
j = 0;
}
/* If no %s found then just print normally */
if (!p[i])
break;
/* Copy up to the %s, and print that */
strncpy(iter->fmt, p, i);
iter->fmt[i] = '\0';
trace_seq_vprintf(&iter->seq, iter->fmt, ap);
/* Add delta to %pS pointers */
if (p[i+1] == 'p') {
unsigned long addr;
char fmt[4];
fmt[0] = '%';
fmt[1] = 'p';
fmt[2] = p[i+2]; /* Either %ps or %pS */
fmt[3] = '\0';
addr = va_arg(ap, unsigned long);
addr += text_delta;
trace_seq_printf(&iter->seq, fmt, (void *)addr);
p += i + 3;
continue; continue;
}
/* str = *(const char **)(ptr + field->offset);
* If iter->seq is full, the above call no longer guarantees
* that ap is in sync with fmt processing, and further calls
* to va_arg() can return wrong positional arguments.
*
* Ensure that ap is no longer used in this case.
*/
if (iter->seq.full) {
p = "";
break;
}
if (star) good = trace_safe_str(iter, str);
len = va_arg(ap, int);
/* The ap now points to the string data of the %s */
str = va_arg(ap, const char *);
good = trace_safe_str(iter, str, star, len);
/* Could be from the last boot */
if (data_delta && !good) {
str += data_delta;
good = trace_safe_str(iter, str, star, len);
}
/* /*
* If you hit this warning, it is likely that the * If you hit this warning, it is likely that the
@ -3846,44 +3729,14 @@ void trace_check_vprintf(struct trace_iterator *iter, const char *fmt,
* instead. See samples/trace_events/trace-events-sample.h * instead. See samples/trace_events/trace-events-sample.h
* for reference. * for reference.
*/ */
if (WARN_ONCE(!good, "fmt: '%s' current_buffer: '%s'", if (WARN_ONCE(!good, "event '%s' has unsafe pointer field '%s'",
fmt, seq_buf_str(&iter->seq.seq))) { trace_event_name(event), field->name)) {
int ret; trace_seq_printf(seq, "EVENT %s: HAS UNSAFE POINTER FIELD '%s'\n",
trace_event_name(event), field->name);
/* Try to safely read the string */ return true;
if (star) {
if (len + 1 > iter->fmt_size)
len = iter->fmt_size - 1;
if (len < 0)
len = 0;
ret = copy_from_kernel_nofault(iter->fmt, str, len);
iter->fmt[len] = 0;
star = false;
} else {
ret = strncpy_from_kernel_nofault(iter->fmt, str,
iter->fmt_size);
}
if (ret < 0)
trace_seq_printf(&iter->seq, "(0x%px)", str);
else
trace_seq_printf(&iter->seq, "(0x%px:%s)",
str, iter->fmt);
str = "[UNSAFE-MEMORY]";
strcpy(iter->fmt, "%s");
} else {
strncpy(iter->fmt, p + i, j + 1);
iter->fmt[j+1] = '\0';
} }
if (star)
trace_seq_printf(&iter->seq, iter->fmt, len, str);
else
trace_seq_printf(&iter->seq, iter->fmt, str);
p += i + j + 1;
} }
print: return false;
if (*p)
trace_seq_vprintf(&iter->seq, p, ap);
} }
const char *trace_event_format(struct trace_iterator *iter, const char *fmt) const char *trace_event_format(struct trace_iterator *iter, const char *fmt)
@ -10777,8 +10630,6 @@ __init static int tracer_alloc_buffers(void)
register_snapshot_cmd(); register_snapshot_cmd();
test_can_verify();
return 0; return 0;
out_free_pipe_cpumask: out_free_pipe_cpumask:

View File

@ -667,9 +667,8 @@ void trace_buffer_unlock_commit_nostack(struct trace_buffer *buffer,
bool trace_is_tracepoint_string(const char *str); bool trace_is_tracepoint_string(const char *str);
const char *trace_event_format(struct trace_iterator *iter, const char *fmt); const char *trace_event_format(struct trace_iterator *iter, const char *fmt);
void trace_check_vprintf(struct trace_iterator *iter, const char *fmt,
va_list ap) __printf(2, 0);
char *trace_iter_expand_format(struct trace_iterator *iter); char *trace_iter_expand_format(struct trace_iterator *iter);
bool ignore_event(struct trace_iterator *iter);
int trace_empty(struct trace_iterator *iter); int trace_empty(struct trace_iterator *iter);
@ -1413,7 +1412,8 @@ struct ftrace_event_field {
int filter_type; int filter_type;
int offset; int offset;
int size; int size;
int is_signed; unsigned int is_signed:1;
unsigned int needs_test:1;
int len; int len;
}; };

View File

@ -82,7 +82,7 @@ static int system_refcount_dec(struct event_subsystem *system)
} }
static struct ftrace_event_field * static struct ftrace_event_field *
__find_event_field(struct list_head *head, char *name) __find_event_field(struct list_head *head, const char *name)
{ {
struct ftrace_event_field *field; struct ftrace_event_field *field;
@ -114,7 +114,8 @@ trace_find_event_field(struct trace_event_call *call, char *name)
static int __trace_define_field(struct list_head *head, const char *type, static int __trace_define_field(struct list_head *head, const char *type,
const char *name, int offset, int size, const char *name, int offset, int size,
int is_signed, int filter_type, int len) int is_signed, int filter_type, int len,
int need_test)
{ {
struct ftrace_event_field *field; struct ftrace_event_field *field;
@ -133,6 +134,7 @@ static int __trace_define_field(struct list_head *head, const char *type,
field->offset = offset; field->offset = offset;
field->size = size; field->size = size;
field->is_signed = is_signed; field->is_signed = is_signed;
field->needs_test = need_test;
field->len = len; field->len = len;
list_add(&field->link, head); list_add(&field->link, head);
@ -151,13 +153,13 @@ int trace_define_field(struct trace_event_call *call, const char *type,
head = trace_get_fields(call); head = trace_get_fields(call);
return __trace_define_field(head, type, name, offset, size, return __trace_define_field(head, type, name, offset, size,
is_signed, filter_type, 0); is_signed, filter_type, 0, 0);
} }
EXPORT_SYMBOL_GPL(trace_define_field); EXPORT_SYMBOL_GPL(trace_define_field);
static int trace_define_field_ext(struct trace_event_call *call, const char *type, static int trace_define_field_ext(struct trace_event_call *call, const char *type,
const char *name, int offset, int size, int is_signed, const char *name, int offset, int size, int is_signed,
int filter_type, int len) int filter_type, int len, int need_test)
{ {
struct list_head *head; struct list_head *head;
@ -166,13 +168,13 @@ static int trace_define_field_ext(struct trace_event_call *call, const char *typ
head = trace_get_fields(call); head = trace_get_fields(call);
return __trace_define_field(head, type, name, offset, size, return __trace_define_field(head, type, name, offset, size,
is_signed, filter_type, len); is_signed, filter_type, len, need_test);
} }
#define __generic_field(type, item, filter_type) \ #define __generic_field(type, item, filter_type) \
ret = __trace_define_field(&ftrace_generic_fields, #type, \ ret = __trace_define_field(&ftrace_generic_fields, #type, \
#item, 0, 0, is_signed_type(type), \ #item, 0, 0, is_signed_type(type), \
filter_type, 0); \ filter_type, 0, 0); \
if (ret) \ if (ret) \
return ret; return ret;
@ -181,7 +183,8 @@ static int trace_define_field_ext(struct trace_event_call *call, const char *typ
"common_" #item, \ "common_" #item, \
offsetof(typeof(ent), item), \ offsetof(typeof(ent), item), \
sizeof(ent.item), \ sizeof(ent.item), \
is_signed_type(type), FILTER_OTHER, 0); \ is_signed_type(type), FILTER_OTHER, \
0, 0); \
if (ret) \ if (ret) \
return ret; return ret;
@ -332,6 +335,7 @@ static bool process_pointer(const char *fmt, int len, struct trace_event_call *c
/* Return true if the string is safe */ /* Return true if the string is safe */
static bool process_string(const char *fmt, int len, struct trace_event_call *call) static bool process_string(const char *fmt, int len, struct trace_event_call *call)
{ {
struct trace_event_fields *field;
const char *r, *e, *s; const char *r, *e, *s;
e = fmt + len; e = fmt + len;
@ -372,8 +376,16 @@ static bool process_string(const char *fmt, int len, struct trace_event_call *ca
if (process_pointer(fmt, len, call)) if (process_pointer(fmt, len, call))
return true; return true;
/* Make sure the field is found, and consider it OK for now if it is */ /* Make sure the field is found */
return find_event_field(fmt, call) != NULL; field = find_event_field(fmt, call);
if (!field)
return false;
/* Test this field's string before printing the event */
call->flags |= TRACE_EVENT_FL_TEST_STR;
field->needs_test = 1;
return true;
} }
/* /*
@ -2586,7 +2598,7 @@ event_define_fields(struct trace_event_call *call)
ret = trace_define_field_ext(call, field->type, field->name, ret = trace_define_field_ext(call, field->type, field->name,
offset, field->size, offset, field->size,
field->is_signed, field->filter_type, field->is_signed, field->filter_type,
field->len); field->len, field->needs_test);
if (WARN_ON_ONCE(ret)) { if (WARN_ON_ONCE(ret)) {
pr_err("error code is %d\n", ret); pr_err("error code is %d\n", ret);
break; break;

View File

@ -317,10 +317,14 @@ EXPORT_SYMBOL(trace_raw_output_prep);
void trace_event_printf(struct trace_iterator *iter, const char *fmt, ...) void trace_event_printf(struct trace_iterator *iter, const char *fmt, ...)
{ {
struct trace_seq *s = &iter->seq;
va_list ap; va_list ap;
if (ignore_event(iter))
return;
va_start(ap, fmt); va_start(ap, fmt);
trace_check_vprintf(iter, trace_event_format(iter, fmt), ap); trace_seq_vprintf(s, trace_event_format(iter, fmt), ap);
va_end(ap); va_end(ap);
} }
EXPORT_SYMBOL(trace_event_printf); EXPORT_SYMBOL(trace_event_printf);