Linux Trace Kernel
 help / color / mirror / Atom feed
* [PATCH] ftrace: Take trace_array reference before accessing its ftrace_ops
@ 2026-08-28 19:59 Steven Rostedt
  2026-08-28 20:13 ` sashiko-bot
  0 siblings, 1 reply; 3+ messages in thread
From: Steven Rostedt @ 2026-08-28 19:59 UTC (permalink / raw)
  To: LKML, Linux Trace Kernel
  Cc: Masami Hiramatsu, Mathieu Desnoyers, Mark Rutland, Breno Leitao

From: Steven Rostedt <rostedt@goodmis.org>

The trace instance files set_ftrace_filter and set_ftrace_notrace was
updated to work with specific trace instances (trace_arrays). The issue is
that when these files are opened, there is a small race window where it
will use the ftrace_ops from the inode->private pointer to get a reference
to the trace_array and then take its reference. The problem is that the
ftrace_ops itself could be freed. If the rmdir on the instance happens at
the same time the set_ftrace_filter file is opened, the rmdir could have
also freed the ftrace_ops and referencing it will cause a use-after-free
bug and crash the kernel.

Instead, pass in the trace_array as the file private data (NULL for the
top level instance), and then pass both the trace_array and the ftrace_ops
to the ftrace_regex_open() function. If the trace_array is NULL, then it
just uses the ftrace_ops without the need to take its reference (like
normal). If the ftrace_ops is NULL, that is only the case for the top
level instance and the global_ops can be used.

This allows the trace_array to have its reference incremented before
touching the ftrace_ops that could also be freed when the instance is.

Cc: stable@vger.kernel.org
Fixes: 591dffdade9f0 ("ftrace: Allow for function tracing instance to filter functions")
Reported-by: Breno Leitao <leitao@debian.org>
Closes: https://lore.kernel.org/all/apGORjltZgAiAYHT@gmail.com/
Signed-off-by: Steven Rostedt <rostedt@goodmis.org>
---
 include/linux/ftrace.h         |  5 ++--
 kernel/trace/ftrace.c          | 52 ++++++++++++++++++++--------------
 kernel/trace/trace.h           |  5 ++--
 kernel/trace/trace_functions.c |  2 +-
 kernel/trace/trace_stack.c     |  2 +-
 5 files changed, 39 insertions(+), 27 deletions(-)

diff --git a/include/linux/ftrace.h b/include/linux/ftrace.h
index 02bc5027523a..bd76a16a63af 100644
--- a/include/linux/ftrace.h
+++ b/include/linux/ftrace.h
@@ -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; })
diff --git a/kernel/trace/ftrace.c b/kernel/trace/ftrace.c
index f9d80c7bd9f1..4babd86c7be0 100644
--- a/kernel/trace/ftrace.c
+++ b/kernel/trace/ftrace.c
@@ -4677,7 +4677,8 @@ ftrace_avail_addrs_open(struct inode *inode, struct file *file)
 
 /**
  * ftrace_regex_open - initialize function tracer filter files
- * @ops: The ftrace_ops that hold the hash filters
+ * @tr: The trace_array that holds the ftrace_ops [optional]
+ * @ops: The ftrace_ops that hold the hash filters [optional]
  * @flag: The type of filter to process
  * @inode: The inode, usually passed in to your open routine
  * @file: The file, usually passed in to your open routine
@@ -4691,26 +4692,38 @@ ftrace_avail_addrs_open(struct inode *inode, struct file *file)
  * tracing_lseek() should be used as the lseek routine, and
  * release must call ftrace_regex_release().
  *
+ * Note, If @tr is not NULL, its reference has to be taken before
+ *       @ops may be referenced.
+ *       If @ops is NULL and @tr is not, then @tr->ops is used.
+ *       If both @tr and @ops are NULL, then the &global_ops is
+ *       to be used.
+ *
  * Returns: 0 on success or a negative errno value on failure
  */
 int
-ftrace_regex_open(struct ftrace_ops *ops, int flag,
+ftrace_regex_open(struct trace_array *tr, struct ftrace_ops *ops, int flag,
 		  struct inode *inode, struct file *file)
 {
-	struct ftrace_iterator *iter;
+	struct ftrace_iterator *iter = NULL;
 	struct ftrace_hash *hash;
 	struct list_head *mod_head;
-	struct trace_array *tr = ops->private;
-	int ret = -ENOMEM;
-
-	ftrace_ops_init(ops);
+	int ret = -ENODEV;
 
 	if (unlikely(ftrace_disabled))
 		return -ENODEV;
 
-	if (tracing_check_open_get_tr(tr))
+	if (tr && tracing_check_open_get_tr(tr))
 		return -ENODEV;
 
+	if (!ops)
+		ops = tr ? tr->ops : &global_ops;
+
+	if (WARN_ON_ONCE(!ops))
+		goto out;
+
+	ftrace_ops_init(ops);
+
+	ret = -ENOMEM;
 	iter = kzalloc_obj(*iter);
 	if (!iter)
 		goto out;
@@ -4788,21 +4801,19 @@ 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;
 
-	/* Checks for tracefs lockdown */
-	return ftrace_regex_open(ops,
-			FTRACE_ITER_FILTER | FTRACE_ITER_DO_PROBES,
-			inode, file);
+	return ftrace_regex_open(tr, NULL,
+				 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,
+	return ftrace_regex_open(tr, NULL, FTRACE_ITER_NOTRACE,
 				 inode, file);
 }
 
@@ -7492,15 +7503,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,7 +7536,6 @@ void ftrace_destroy_filter_files(struct ftrace_ops *ops)
 
 static __init int ftrace_init_dyn_tracefs(struct dentry *d_tracer)
 {
-
 	trace_create_file("available_filter_functions", TRACE_MODE_READ,
 			d_tracer, NULL, &ftrace_avail_fops);
 
@@ -7538,7 +7548,7 @@ 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);
+	ftrace_create_filter_files(NULL, d_tracer);
 
 #ifdef CONFIG_FUNCTION_GRAPH_TRACER
 	trace_create_file("set_graph_function", TRACE_MODE_WRITE, d_tracer,
diff --git a/kernel/trace/trace.h b/kernel/trace/trace.h
index 74a7a50d1e78..3c111ca88e32 100644
--- a/kernel/trace/trace.h
+++ b/kernel/trace/trace.h
@@ -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 */
 
diff --git a/kernel/trace/trace_functions.c b/kernel/trace/trace_functions.c
index cd37f2013758..c879d43a5fbb 100644
--- a/kernel/trace/trace_functions.c
+++ b/kernel/trace/trace_functions.c
@@ -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;
 }
diff --git a/kernel/trace/trace_stack.c b/kernel/trace/trace_stack.c
index 0aa2514a6593..e7f4e523587d 100644
--- a/kernel/trace/trace_stack.c
+++ b/kernel/trace/trace_stack.c
@@ -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);
 }
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* Re: [PATCH] ftrace: Take trace_array reference before accessing its ftrace_ops
  2026-08-28 19:59 [PATCH] ftrace: Take trace_array reference before accessing its ftrace_ops Steven Rostedt
@ 2026-08-28 20:13 ` sashiko-bot
  2026-08-29  2:22   ` Steven Rostedt
  0 siblings, 1 reply; 3+ messages in thread
From: sashiko-bot @ 2026-08-28 20:13 UTC (permalink / raw)
  To: Steven Rostedt; +Cc: linux-trace-kernel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] Tracefs lockdown and tracing_disabled checks are bypassed for the global ftrace instance and stack tracer.
- [High] Module and probe filters for the global ftrace instance cannot be cleared or read.
--

commit 42c008f904dcd516dc1681a548fae6a4bba9a162
Author: Steven Rostedt <rostedt@goodmis.org>

ftrace: Take trace_array reference before accessing its ftrace_ops

The trace instance files set_ftrace_filter and set_ftrace_notrace are
updated to work with specific trace instances (trace_arrays). This
patch passes the trace_array as the file private data to allow incrementing
its reference before touching the ftrace_ops, preventing a use-after-free.

Closes: https://lore.kernel.org/all/apGORjltZgAiAYHT@gmail.com/

> diff --git a/kernel/trace/ftrace.c b/kernel/trace/ftrace.c
> index f9d80c7bd9f16..4babd86c7be00 100644
> --- a/kernel/trace/ftrace.c
> +++ b/kernel/trace/ftrace.c

[ ... ]

>  int
> -ftrace_regex_open(struct ftrace_ops *ops, int flag,
> +ftrace_regex_open(struct trace_array *tr, struct ftrace_ops *ops, int flag,
>  		  struct inode *inode, struct file *file)
>  {
> -	struct ftrace_iterator *iter;
> +	struct ftrace_iterator *iter = NULL;
>  	struct ftrace_hash *hash;
>  	struct list_head *mod_head;
> -	struct trace_array *tr = ops->private;
> -	int ret = -ENOMEM;
> -
> -	ftrace_ops_init(ops);
> +	int ret = -ENODEV;
>  
>  	if (unlikely(ftrace_disabled))
>  		return -ENODEV;
>  
> -	if (tracing_check_open_get_tr(tr))
> +	if (tr && tracing_check_open_get_tr(tr))
>  		return -ENODEV;

[Severity: Critical]
By adding the `tr &&` short-circuit here, does this cause a regression
where tracefs lockdown and tracing_disabled checks are bypassed for the
global ftrace instance and the stack tracer?

Since `tr` is explicitly passed as `NULL` for these instances,
`tracing_check_open_get_tr(NULL)` is never called.

[ ... ]

> @@ -4788,21 +4801,19 @@ 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;
>  
> -	/* Checks for tracefs lockdown */
> -	return ftrace_regex_open(ops,
> -			FTRACE_ITER_FILTER | FTRACE_ITER_DO_PROBES,
> -			inode, file);
> +	return ftrace_regex_open(tr, NULL,
> +				 FTRACE_ITER_FILTER | FTRACE_ITER_DO_PROBES,
> +				 inode, file);
>  }

[Severity: High]
For the global ftrace instance, `tr` is `NULL`. Does passing `NULL` for `tr`
break the module and probe filters?

In `ftrace_regex_open()`, the `mod_head` pointer is initialized like this:

    mod_head = tr ? &tr->mod_trace : NULL;

Because `tr` is `NULL`, `mod_head` will also be `NULL`. When opening the
file with `O_TRUNC`, `clear_ftrace_mod_list(mod_head)` will do nothing,
failing to clear the module filters.

Additionally, `iter->tr` is set to `NULL`. When reading the file,
`t_mod_start()` relies on `iter->tr` and will return early:

    /* probes are only available if tr is set */
    if (!iter->tr)
        return NULL;

Could this skip the enumeration of active module and probe filters for the
global instance entirely?

[ ... ]

> diff --git a/kernel/trace/trace_stack.c b/kernel/trace/trace_stack.c
> index 0aa2514a6593b..e7f4e523587d0 100644
> --- a/kernel/trace/trace_stack.c
> +++ b/kernel/trace/trace_stack.c
> @@ -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);
>  }

[Severity: Critical]
The stack tracer explicitly passes `NULL` for the trace array. Due to the
short-circuit mentioned earlier, doesn't this also bypass the lockdown
check documented in the comment right above this call?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260828155942.445f46e5@gandalf.local.home?part=1

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [PATCH] ftrace: Take trace_array reference before accessing its ftrace_ops
  2026-08-28 20:13 ` sashiko-bot
@ 2026-08-29  2:22   ` Steven Rostedt
  0 siblings, 0 replies; 3+ messages in thread
From: Steven Rostedt @ 2026-08-29  2:22 UTC (permalink / raw)
  To: sashiko-bot; +Cc: sashiko-reviews, linux-trace-kernel

On Fri, 28 Aug 2026 20:13:09 +0000
sashiko-bot@kernel.org wrote:

> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [Critical] Tracefs lockdown and tracing_disabled checks are bypassed for the global ftrace instance and stack tracer.
> - [High] Module and probe filters for the global ftrace instance cannot be cleared or read.
> --
> 
> commit 42c008f904dcd516dc1681a548fae6a4bba9a162
> Author: Steven Rostedt <rostedt@goodmis.org>
> 
> ftrace: Take trace_array reference before accessing its ftrace_ops
> 
> The trace instance files set_ftrace_filter and set_ftrace_notrace are
> updated to work with specific trace instances (trace_arrays). This
> patch passes the trace_array as the file private data to allow incrementing
> its reference before touching the ftrace_ops, preventing a use-after-free.
> 
> Closes: https://lore.kernel.org/all/apGORjltZgAiAYHT@gmail.com/
> 
> > diff --git a/kernel/trace/ftrace.c b/kernel/trace/ftrace.c
> > index f9d80c7bd9f16..4babd86c7be00 100644
> > --- a/kernel/trace/ftrace.c
> > +++ b/kernel/trace/ftrace.c  
> 
> [ ... ]
> 
> >  int
> > -ftrace_regex_open(struct ftrace_ops *ops, int flag,
> > +ftrace_regex_open(struct trace_array *tr, struct ftrace_ops *ops, int flag,
> >  		  struct inode *inode, struct file *file)
> >  {
> > -	struct ftrace_iterator *iter;
> > +	struct ftrace_iterator *iter = NULL;
> >  	struct ftrace_hash *hash;
> >  	struct list_head *mod_head;
> > -	struct trace_array *tr = ops->private;
> > -	int ret = -ENOMEM;
> > -
> > -	ftrace_ops_init(ops);
> > +	int ret = -ENODEV;
> >  
> >  	if (unlikely(ftrace_disabled))
> >  		return -ENODEV;
> >  
> > -	if (tracing_check_open_get_tr(tr))
> > +	if (tr && tracing_check_open_get_tr(tr))
> >  		return -ENODEV;  
> 
> [Severity: Critical]
> By adding the `tr &&` short-circuit here, does this cause a regression
> where tracefs lockdown and tracing_disabled checks are bypassed for the
> global ftrace instance and the stack tracer?
> 
> Since `tr` is explicitly passed as `NULL` for these instances,
> `tracing_check_open_get_tr(NULL)` is never called.

Yeah, right after sending this I noticed that the if statement isn't needed.

> 
> [ ... ]
> 
> > @@ -4788,21 +4801,19 @@ 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;
> >  
> > -	/* Checks for tracefs lockdown */
> > -	return ftrace_regex_open(ops,
> > -			FTRACE_ITER_FILTER | FTRACE_ITER_DO_PROBES,
> > -			inode, file);
> > +	return ftrace_regex_open(tr, NULL,
> > +				 FTRACE_ITER_FILTER | FTRACE_ITER_DO_PROBES,
> > +				 inode, file);
> >  }  
> 
> [Severity: High]
> For the global ftrace instance, `tr` is `NULL`. Does passing `NULL` for `tr`
> break the module and probe filters?
> 
> In `ftrace_regex_open()`, the `mod_head` pointer is initialized like this:
> 
>     mod_head = tr ? &tr->mod_trace : NULL;
> 
> Because `tr` is `NULL`, `mod_head` will also be `NULL`. When opening the
> file with `O_TRUNC`, `clear_ftrace_mod_list(mod_head)` will do nothing,
> failing to clear the module filters.
> 
> Additionally, `iter->tr` is set to `NULL`. When reading the file,
> `t_mod_start()` relies on `iter->tr` and will return early:
> 
>     /* probes are only available if tr is set */
>     if (!iter->tr)
>         return NULL;
> 
> Could this skip the enumeration of active module and probe filters for the
> global instance entirely?

Hmm, yeah.

I guess I need to add a:

perhaps I need to add at the start:

	if (!tr && ops)
		tr = ops->private;

> 
> [ ... ]
> 
> > diff --git a/kernel/trace/trace_stack.c b/kernel/trace/trace_stack.c
> > index 0aa2514a6593b..e7f4e523587d0 100644
> > --- a/kernel/trace/trace_stack.c
> > +++ b/kernel/trace/trace_stack.c
> > @@ -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);
> >  }  
> 
> [Severity: Critical]
> The stack tracer explicitly passes `NULL` for the trace array. Due to the
> short-circuit mentioned earlier, doesn't this also bypass the lockdown
> check documented in the comment right above this call?
> 

Yes, and I mentioned at the start I need to remove the if statement.

-- Steve


^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-08-29  2:22 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-28 19:59 [PATCH] ftrace: Take trace_array reference before accessing its ftrace_ops Steven Rostedt
2026-08-28 20:13 ` sashiko-bot
2026-08-29  2:22   ` Steven Rostedt

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox