ftrace: UAF in ftrace_regex_open() on a removed tracing instance
I am seeing some UAF KASAN issue on ftrace in Meta prod, and I think I got a reproducer that works ok.
BUG: KASAN: slab-use-after-free in ftrace_regex_open+0x50/0x770
Read of size 8 at addr ffff00097627f418 by task stress-ng-fanot/1811644
CPU: 22 UID: 0 PID: 1811644 Comm: stress-ng-fanot Kdump: loaded Tainted: G E 7.2.0-next-20260827upstream #2
PREEMPT(full)
Call trace:
show_stack+0x20/0x38 (C)
__dump_stack+0x28/0x38
dump_stack_lvl+0x74/0xa8
print_report+0xd8/0x250
kasan_report+0x98/0xf8
__asan_report_load8_noabort+0x20/0x30
ftrace_regex_open+0x50/0x770
ftrace_filter_open+0x44/0x58
do_dentry_open+0x308/0xd20
vfs_open+0x44/0x280
dentry_open_nonotify+0x68/0xc0
fanotify_read+0x5c0/0x1d78
vfs_read+0x198/0x6f0
ksys_read+0xbc/0x168
__arm64_sys_read+0x88/0xa8
invoke_syscall+0x74/0x188
do_el0_svc+0x108/0x190
el0_svc+0x64/0x260
el0t_64_sync_handler+0x68/0xe0
el0t_64_sync+0x198/0x1a0
Allocated by task 1808767:
kasan_save_track+0x30/0x68
kasan_save_alloc_info+0x40/0x50
__kasan_kmalloc+0x84/0xa0
__kmalloc_cache_noprof+0x2b4/0x588
ftrace_allocate_ftrace_ops+0x84/0x148
trace_array_create_systems+0x470/0x13e8
instance_mkdir+0xcc/0x118
tracefs_syscall_mkdir+0xa0/0xf0
vfs_mkdir+0x2a0/0x3d8
filename_mkdirat+0x1c4/0x368
__arm64_sys_mkdirat+0xa4/0x198
invoke_syscall+0x74/0x188
do_el0_svc+0x108/0x190
el0_svc+0x64/0x260
el0t_64_sync_handler+0x68/0xe0
el0t_64_sync+0x198/0x1a0
Freed by task 1812034:
kasan_save_track+0x30/0x68
kasan_save_free_info+0x54/0x70
__kasan_slab_free+0x58/0x88
kfree+0x194/0x6b0
ftrace_destroy_function_files+0x5c/0x90
__remove_instance+0x424/0xa68
instance_rmdir+0xb8/0xf8
tracefs_syscall_rmdir+0x84/0xf8
vfs_rmdir+0x18c/0x458
filename_rmdir+0x1a0/0x310
__arm64_sys_unlinkat+0xb0/0x1c0
invoke_syscall+0x74/0x188
do_el0_svc+0x108/0x190
el0_svc+0x64/0x260
el0t_64_sync_handler+0x68/0xe0
el0t_64_sync+0x198/0x1a0
The buggy address belongs to the object at ffff00097627f400
which belongs to the cache kmalloc-512 of size 512
The buggy address is located 24 bytes inside of
freed 512-byte region [ffff00097627f400, ffff00097627f600)
The buggy address belongs to the physical page:
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x9f6278
head: order:3 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0
flags: 0x5ffff0000000040(head|node=0|zone=2|lastcpupid=0x1ffff)
page_type: f5(slab)
raw: 05ffff0000000040 ffff000080008c80 dead000000000100 dead000000000122
raw: 0000000000000000 0000000000200020 00000000f5000000 0000000000000000
head: 05ffff0000000040 ffff000080008c80 dead000000000100 dead000000000122
head: 0000000000000000 0000000000200020 00000000f5000000 0000000000000000
head: 05ffff0000000003 fffffdffe5d89e01 00000000ffffffff 00000000ffffffff
head: ffffffffffffffff 0000000000000000 00000000ffffffff 0000000000000008
page dumped because: kasan: bad access detected
Memory state around the buggy address:
ffff00097627f300: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
ffff00097627f380: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
>ffff00097627f400: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
^
ffff00097627f480: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
ffff00097627f500: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
==================================================================Reproducer
----------
Needs root and CONFIG_KASAN=y, CONFIG_FANOTIFY=y, CONFIG_DYNAMIC_FTRACE=y. It is deterministic and fires on the first read():
It seems it was introduced by 591dffdade9f ("ftrace: Allow for function tracing instance to filter functions") in v3.15, so all stable trees are affected. A fix is attached.
Thanks,
Breno
---
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/fanotify.h>
#include <sys/stat.h>
#define TRACEFS "/sys/kernel/tracing"
#define INSTANCE TRACEFS "/instances/uaf"
#define TARGET INSTANCE "/set_ftrace_filter"
int main(void)
{
char buf[4096];
int fan, fd;
fan = fanotify_init(FAN_CLASS_NOTIF, O_RDONLY);
if (fan < 0)
return perror("fanotify_init"), 1;
if (fanotify_mark(fan, FAN_MARK_ADD | FAN_MARK_MOUNT, FAN_OPEN,
AT_FDCWD, TRACEFS) < 0)
return perror("fanotify_mark"), 1;
rmdir(INSTANCE);
if (mkdir(INSTANCE, 0755) < 0)
return perror("mkdir"), 1;
/* Queues an event; fanotify does path_get() on the file. */
fd = open(TARGET, O_RDONLY);
if (fd < 0)
return perror(TARGET), 1;
close(fd);
/* Frees tr->ops while fanotify still pins the path. */
if (rmdir(INSTANCE) < 0)
return perror("rmdir"), 1;
/* create_fd() -> dentry_open_nonotify() -> ftrace_filter_open(). */
if (read(fan, buf, sizeof(buf)) < 0)
perror("read");
return 0;
}Steven Rostedt
maintainerQuoted 10 lines · On Fri, 28 Aug 2026 06:38:17 -0700 Breno Leitao <[email protected]> wrote:
I am seeing some UAF KASAN issue on ftrace in Meta prod, and I think I got a reproducer that works ok.
BUG: KASAN: slab-use-after-free in ftrace_regex_open+0x50/0x770 Read of size 8 at addr ffff00097627f418 by task stress-ng-fanot/1811644#define TRACEFS "
/sys/kernel/tracing"
#define INSTANCE TRACEFS "/instances/uaf"
#define TARGET INSTANCE "/set_ftrace_filter"
Ugg, this is similar to the bug I just fixed[1]. But this one will not be so trivial to solve. As the trace_array in question owns the ftrace_ops that is used to register the set_ftrace_filter file. Upping the reference to the trace_array will require some thought.
Let me look deeper at it.
-- Steve
Quoted 14 lines · Steven Rostedt
Quoted 10 lines · On Fri, 28 Aug 2026 06:38:17 -0700 Breno Leitao <[email protected]> wrote:
I am seeing some UAF KASAN issue on ftrace in Meta prod, and I think I got a reproducer that works ok.
BUG: KASAN: slab-use-after-free in ftrace_regex_open+0x50/0x770 Read of size 8 at addr ffff00097627f418 by task stress-ng-fanot/1811644#define TRACEFS "
/sys/kernel/tracing"
#define INSTANCE TRACEFS "/instances/uaf"
#define TARGET INSTANCE "/set_ftrace_filter"
Quoted 6 lines
Ugg, this is similar to the bug I just fixed[1]. But this one will not be so trivial to solve. As the
trace_arrayin question owns theftrace_opsthat is used to register theset_ftrace_filterfile. Upping the reference to thetrace_arraywill require some thought.Let me look deeper at it.
My LLM came up with something like the following, and it doesn't reproduce the problem anymore. Very similar to your code above:
kernel/trace/ftrace.c +30 -11
@@ -4788,22 +4788,37 @@ ftrace_regex_open(struct ftrace_ops *ops, int flag,static int ftrace_filter_open (struct inode *inode,struct file *file) { -struct ftrace_ops *ops = inode->i_private; +struct trace_array *tr = inode->i_private; +int ret; -/* Checks for tracefs lockdown */ -return ftrace_regex_open (ops, -FTRACE_ITER_FILTER |FTRACE_ITER_DO_PROBES , - inode, file); +/* Checks for tracefs lockdown, and that the instance is still alive */ + ret =tracing_check_open_get_tr (tr); +if (ret) +return ret; + + ret =ftrace_regex_open (tr->ops, +FTRACE_ITER_FILTER |FTRACE_ITER_DO_PROBES , + inode, file); +/* ftrace_regex_open() holds its own reference on success */ +trace_array_put (tr); +return ret; }static int ftrace_notrace_open (struct inode *inode,struct file *file) { -struct ftrace_ops *ops = inode->i_private; +struct trace_array *tr = inode->i_private; +int ret; + +/* Checks for tracefs lockdown, and that the instance is still alive */ + ret =tracing_check_open_get_tr (tr); +if (ret) +return ret; -/* Checks for tracefs lockdown */ -return ftrace_regex_open (ops,FTRACE_ITER_NOTRACE , - inode, file); + ret =ftrace_regex_open (tr->ops,FTRACE_ITER_NOTRACE , inode, file); +/* ftrace_regex_open() holds its own reference on success */ +trace_array_put (tr); +return ret; }/* Type for quick search ftrace basic regexes (globs) from filter_parse_regex */
@@ -7496,11 +7511,15 @@ void ftrace_create_filter_files(struct ftrace_ops *ops, struct dentry *parent) { +/* +* Pass the trace array and not @ops, as @ops is freed when the +* instance is removed, but the trace array can still be validated. +*/ trace_create_file ("set_ftrace_filter" ,TRACE_MODE_WRITE , parent, - ops, &ftrace_filter_fops); + ops->private, &ftrace_filter_fops);trace_create_file ("set_ftrace_notrace" ,TRACE_MODE_WRITE , parent, - ops, &ftrace_notrace_fops); + ops->private, &ftrace_notrace_fops); }/*
Steven Rostedt
maintainerQuoted 3 lines · On Fri, 28 Aug 2026 08:59:39 -0700 Breno Leitao <[email protected]> wrote:
My LLM came up with something like the following, and it doesn't reproduce the problem anymore. Very similar to your code above:
I know the solution but there's subtle things I want to make sure that works.
Quoted 53 lines
kernel/trace/ftrace.c +24
-9@@ -4788,22 +4788,37 @@ ftrace_regex_open(struct ftrace_ops *ops, int flag,static int ftrace_filter_open (struct inode *inode,struct file *file) { -struct ftrace_ops *ops = inode->i_private; +struct trace_array *tr = inode->i_private; +int ret; -/* Checks for tracefs lockdown */ -return ftrace_regex_open (ops, -FTRACE_ITER_FILTER |FTRACE_ITER_DO_PROBES , - inode, file); +/* Checks for tracefs lockdown, and that the instance is still alive */ + ret =tracing_check_open_get_tr (tr); +if (ret) +return ret; + + ret =ftrace_regex_open (tr->ops, +FTRACE_ITER_FILTER |FTRACE_ITER_DO_PROBES , + inode, file); +/* ftrace_regex_open() holds its own reference on success */ +trace_array_put (tr); +return ret; }static int ftrace_notrace_open (struct inode *inode,struct file *file) { -struct ftrace_ops *ops = inode->i_private; +struct trace_array *tr = inode->i_private; +int ret; + +/* Checks for tracefs lockdown, and that the instance is still alive */ + ret =tracing_check_open_get_tr (tr); +if (ret) +return ret; -/* Checks for tracefs lockdown */ -return ftrace_regex_open (ops,FTRACE_ITER_NOTRACE , - inode, file); + ret =ftrace_regex_open (tr->ops,FTRACE_ITER_NOTRACE , inode, file); +/* ftrace_regex_open() holds its own reference on success */ +trace_array_put (tr); +return ret; }/* Type for quick search ftrace basic regexes (globs) from filter_parse_regex */
kernel/trace/ftrace.c void ftrace_create_filter_files(struct ftrace_ops *ops, · line 7517
Quoted 7 lines
kernel/trace/ftrace.c +4
-0@@ -7496,11 +7511,15 @@ void ftrace_create_filter_files(struct ftrace_ops *ops, struct dentry *parent) { +/* +* Pass the trace array and not @ops, as @ops is freed when the +* instance is removed, but the trace array can still be validated. +*/
I hate these comments. It's really pointless to state this. It's a "no shit" comment ;-)
kernel/trace/ftrace.c void ftrace_create_filter_files(struct ftrace_ops *ops, · line 7519
Quoted 3 lines
kernel/trace/ftrace.c +1
-1trace_create_file ("set_ftrace_filter" ,TRACE_MODE_WRITE , parent, - ops, &ftrace_filter_fops); + ops->private, &ftrace_filter_fops);
But this also assumes that ops->private is always a trace_array. Which may someday change and we will get a hidden bug if it does.
-- Steve
Quoted 7 lines
kernel/trace/ftrace.c +1
-1trace_create_file ("set_ftrace_notrace" ,TRACE_MODE_WRITE , parent, - ops, &ftrace_notrace_fops); + ops->private, &ftrace_notrace_fops); }/*
Steven Rostedt
maintainerQuoted 8 lines · On Fri, 28 Aug 2026 12:34:52 -0400 Steven Rostedt <[email protected]> wrote:
Review
Quoted 3 lines
(quoted diff) +1
-1trace_create_file("set_ftrace_filter", TRACE_MODE_WRITE, parent, - ops, &ftrace_filter_fops); + ops->private, &ftrace_filter_fops);But this also assumes that
ops->privateis always atrace_array. Which may someday change and we will get a hidden bug if it does.This is the exact reason I want to look deeper at it.
Something like this is more robust:
kernel/trace/ftrace.c +40 -13
@@ -4786,24 +4786,48 @@ ftrace_regex_open(struct ftrace_ops *ops, int flag, }static int -ftrace_filter_open (struct inode *inode,struct file *file) +__ftrace_regex_open (struct trace_array *tr,int flag, +struct inode *inode,struct file *file) { -struct ftrace_ops *ops = inode->i_private; +struct ftrace_ops *ops; +int ret; + +if (trace_array_get (tr) <0 ) +return -ENODEV ; + + ret = -EINVAL ; + ops = tr->ops; +if (!ops) + goto fail;/* Checks for tracefs lockdown */ -return ftrace_regex_open (ops, -FTRACE_ITER_FILTER |FTRACE_ITER_DO_PROBES , - inode, file); + ret =ftrace_regex_open (ops, flag, inode, file); +if (ret <0 ) + goto fail; + +return ret; + fail: +trace_array_put (tr); +return ret; +} + +static int +ftrace_filter_open (struct inode *inode,struct file *file) +{ +struct trace_array *tr = inode->i_private; + +return __ftrace_regex_open (tr, +FTRACE_ITER_FILTER |FTRACE_ITER_DO_PROBES , + inode, file); }static int ftrace_notrace_open (struct inode *inode,struct file *file) { -struct ftrace_ops *ops = inode->i_private; +struct trace_array *tr = inode->i_private; -/* Checks for tracefs lockdown */ -return ftrace_regex_open (ops,FTRACE_ITER_NOTRACE , - inode, file); +return __ftrace_regex_open (tr,FTRACE_ITER_NOTRACE , + inode, file); }/* Type for quick search ftrace basic regexes (globs) from filter_parse_regex */
@@ -7492,15 +7516,15 @@ static const struct file_operations ftrace_graph_notrace_fops = { };#endif /* CONFIG_FUNCTION_GRAPH_TRACER */ -void ftrace_create_filter_files (struct ftrace_ops *ops, +void ftrace_create_filter_files (struct trace_array *tr,struct dentry *parent) {trace_create_file ("set_ftrace_filter" ,TRACE_MODE_WRITE , parent, - ops, &ftrace_filter_fops); + tr, &ftrace_filter_fops);trace_create_file ("set_ftrace_notrace" ,TRACE_MODE_WRITE , parent, - ops, &ftrace_notrace_fops); + tr, &ftrace_notrace_fops); }/*
@@ -7525,6 +7549,7 @@ void ftrace_destroy_filter_files(struct ftrace_ops *ops)static __init int ftrace_init_dyn_tracefs (struct dentry *d_tracer) { +struct trace_array *tr;trace_create_file ("available_filter_functions" ,TRACE_MODE_READ , d_tracer,NULL , &ftrace_avail_fops);
@@ -7538,7 +7563,9 @@ static __init int ftrace_init_dyn_tracefs(struct dentry *d_tracer)trace_create_file ("touched_functions" ,TRACE_MODE_READ , d_tracer,NULL , &ftrace_touched_fops); -ftrace_create_filter_files (&global_ops, d_tracer); + tr = global_ops.private; +if (!WARN_ON (tr ==NULL )) +ftrace_create_filter_files (tr, d_tracer);#ifdef CONFIG_FUNCTION_GRAPH_TRACER trace_create_file("set_graph_function", TRACE_MODE_WRITE, d_tracer,
kernel/trace/trace.h +3 -2
@@ -1340,7 +1340,7 @@ extern void clear_ftrace_function_probes(struct trace_array *tr);int register_ftrace_command (struct ftrace_func_command *cmd);int unregister_ftrace_command (struct ftrace_func_command *cmd); -void ftrace_create_filter_files (struct ftrace_ops *ops, +void ftrace_create_filter_files (struct trace_array *tr,struct dentry *parent);void ftrace_destroy_filter_files (struct ftrace_ops *ops);
@@ -1363,11 +1363,12 @@ static inline void clear_ftrace_function_probes(struct trace_array *tr) { } +static inline void ftrace_create_filter_files (struct trace_array *tr, +struct dentry *parent) { }/* * The ops parameter passed in is usually undefined. * This must be a macro. */ -#define ftrace_create_filter_files (ops, parent) do { } while (0)#define ftrace_destroy_filter_files (ops) do { } while (0)#endif /* CONFIG_FUNCTION_TRACER && CONFIG_DYNAMIC_FTRACE */
kernel/trace/trace_functions.c +1 -1
@@ -101,7 +101,7 @@ int ftrace_create_function_files(struct trace_array *tr,return ret; } -ftrace_create_filter_files (tr->ops, parent); +ftrace_create_filter_files (tr, parent);return 0 ; }
-- Steve
Steven Rostedt
maintainerQuoted 9 lines · On Fri, 28 Aug 2026 08:59:39 -0700 Breno Leitao <[email protected]> wrote:
ftrace_filter_open(struct inode *inode, struct file *file){kernel/trace/ftrace.c +2
-4-struct ftrace_ops *ops = inode->i_private; +struct trace_array *tr = inode->i_private; +int ret; -/* Checks for tracefs lockdown */ - return ftrace_regex_open(ops, - FTRACE_ITER_FILTER | FTRACE_ITER_DO_PROBES,
kernel/trace/ftrace.c ftrace_regex_open(struct ftrace_ops *ops, int flag, · line 4802
Quoted 10 lines
kernel/trace/ftrace.c +9
-1- inode, file); +/* Checks for tracefs lockdown, and that the instance is still alive */ + ret =tracing_check_open_get_tr (tr); +if (ret) +return ret; + + ret =ftrace_regex_open (tr->ops, +FTRACE_ITER_FILTER |FTRACE_ITER_DO_PROBES , + inode, file); +/* ftrace_regex_open() holds its own reference on success */
Hmm, I just noticed this part (from reviewing my own code first, and then looking at what your AI did).
I guess the issue is that the ops may be freed before you get access to the trace array. Anyway more needs to be changed. I may remove the extra reference. No need to do it twice. Also since trace_stack may have the issue too.
-- Steve
Quoted 3 lines
kernel/trace/ftrace.c +2
-0+trace_array_put (tr); +return ret; }
Steven Rostedt
maintainerQuoted 4 lines · On Fri, 28 Aug 2026 14:04:11 -0400 Steven Rostedt <[email protected]> wrote:
I guess the issue is that the ops may be freed before you get access to the trace array. Anyway more needs to be changed. I may remove the extra reference. No need to do it twice. Also since
trace_stackmay have the issue too.
Something more like this:
include/linux/ftrace.h +3 -2
@@ -866,8 +866,9 @@ unsigned long ftrace_get_addr_new(struct dyn_ftrace *rec);unsigned long ftrace_get_addr_curr (struct dyn_ftrace *rec);extern ftrace_func_t ftrace_trace_function; +struct trace_array ; -int ftrace_regex_open (struct ftrace_ops *ops,int flag, +int ftrace_regex_open (struct trace_array *tr,struct ftrace_ops *ops,int flag,struct inode *inode,struct file *file);ssize_t ftrace_filter_write (struct file *file,const char __user *ubuf,size_t cnt,loff_t *ppos);
@@ -1077,7 +1078,7 @@ static inline unsigned long ftrace_location(unsigned long ip)* have them defined when ftrace is not enabled, but these * functions may still be called. Use a macro instead of inline. */ -#define ftrace_regex_open (ops, flag, inod, file) ({ -ENODEV; }) +#define ftrace_regex_open (tr, ops, flag, inode, file) ({ -ENODEV; })#define ftrace_set_early_filter (ops, buf, enable) do { } while (0)#define ftrace_set_filter_ip (ops, ip, remove, reset) ({ -ENODEV; })#define ftrace_set_filter_ips (ops, ips, cnt, remove, reset) ({ -ENODEV; })
kernel/trace/ftrace.c +24 -20
103 lines not on this page · raw message
kernel/trace/trace.h +3 -2
@@ -1340,7 +1340,7 @@ extern void clear_ftrace_function_probes(struct trace_array *tr);int register_ftrace_command (struct ftrace_func_command *cmd);int unregister_ftrace_command (struct ftrace_func_command *cmd); -void ftrace_create_filter_files (struct ftrace_ops *ops, +void ftrace_create_filter_files (struct trace_array *tr,struct dentry *parent);void ftrace_destroy_filter_files (struct ftrace_ops *ops);
@@ -1363,11 +1363,12 @@ static inline void clear_ftrace_function_probes(struct trace_array *tr) { } +static inline void ftrace_create_filter_files (struct trace_array *tr, +struct dentry *parent) { }/* * The ops parameter passed in is usually undefined. * This must be a macro. */ -#define ftrace_create_filter_files (ops, parent) do { } while (0)#define ftrace_destroy_filter_files (ops) do { } while (0)#endif /* CONFIG_FUNCTION_TRACER && CONFIG_DYNAMIC_FTRACE */
kernel/trace/trace_functions.c +1 -1
@@ -101,7 +101,7 @@ int ftrace_create_function_files(struct trace_array *tr,return ret; } -ftrace_create_filter_files (tr->ops, parent); +ftrace_create_filter_files (tr, parent);return 0 ; }
kernel/trace/trace_stack.c +1 -1
@@ -499,7 +499,7 @@ stack_trace_filter_open(struct inode *inode, struct file *file)struct ftrace_ops *ops = inode->i_private;/* Checks for tracefs lockdown */ -return ftrace_regex_open (ops,FTRACE_ITER_FILTER , +return ftrace_regex_open (NULL , ops,FTRACE_ITER_FILTER , inode, file); }
Signature · 3 lines
2.53.0 -- Steve