* [PATCH 1/2] ftrace: Wait for graph readers before releasing operations
@ 2026-11-03 17:58 Weiming Shi
2026-11-03 17:58 ` [PATCH 2/2] ftrace: Complete subops teardown after hash update failure Weiming Shi
0 siblings, 1 reply; 2+ messages in thread
From: Weiming Shi @ 2026-11-03 17:58 UTC (permalink / raw)
To: Steven Rostedt, Masami Hiramatsu, Mark Rutland, Mathieu Desnoyers
Cc: linux-kernel, linux-trace-kernel, co+bba0c6b49150bef3, Xiang Mei,
Weiming Shi, stable
unregister_ftrace_graph() removes an operation from the graph array before
returning. A graph callback may already hold the operation or its filter,
however, while the caller can release them as soon as unregistration
completes.
This can happen without a hash update failure. For example, consider three
graph operations with identical filters where one also enables PID
filtering. Removing that operation leaves the manager hashes unchanged, so
an in-flight fgraph_pid_func() may call gops->saved_func after unregister
has cleared it.
Graph callbacks run with preemption disabled and may run while RCU is not
watching. Separate removing the operation from graph dispatch from waiting
for callbacks that may still be using it. Keep unregister_ftrace_graph()
synchronous by performing both steps, and require internal users of the
split interface to pair every successful removal with a reader wait.
For fprobes, unpublish the graph operation and release its slot
synchronously, then queue the reader waits and filter release. This keeps
the waits out of unregister_fprobe_async(). Flush the work before reusing
the shared graph operation.
Disable fprobe return dispatch before unpublishing the operation and check
that state inside the callback's RCU critical section. Re-enable dispatch
only after graph registration succeeds, so a return callback cannot use a
released fprobe while cleanup is pending.
Cc: stable@vger.kernel.org
Fixes: df3ec5da6a1e ("function_graph: Add pid tracing back to function graph tracer")
Reported-by: <co+bba0c6b49150bef3@bugs.sh>
Assisted-by: LLM
Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
kernel/trace/fgraph.c | 47 +++++++++++++++++++++++++++++++---
kernel/trace/fprobe.c | 21 ++++++++++++---
kernel/trace/ftrace_internal.h | 2 ++
3 files changed, 62 insertions(+), 8 deletions(-)
diff --git a/kernel/trace/fgraph.c b/kernel/trace/fgraph.c
index ed455b535..14733f519 100644
--- a/kernel/trace/fgraph.c
+++ b/kernel/trace/fgraph.c
@@ -1420,13 +1420,26 @@ int register_ftrace_graph(struct fgraph_ops *gops)
return ret;
}
-void unregister_ftrace_graph(struct fgraph_ops *gops)
+/**
+ * unregister_ftrace_graph_start - unpublish a function graph operation
+ * @gops: graph operation to remove
+ *
+ * Remove @gops from graph dispatch, release its array slot, and update the
+ * graph and subop state. Existing graph callbacks may still be using @gops
+ * after this returns.
+ *
+ * Return: true if @gops was unpublished. The caller must then invoke
+ * unregister_ftrace_graph_finish() exactly once before releasing @gops or its
+ * filter, even if the subop hash update failed.
+ */
+bool unregister_ftrace_graph_start(struct fgraph_ops *gops)
{
int command = 0;
+ bool removed = false;
if (WARN_ONCE(!(gops->ops.flags & FTRACE_OPS_FL_GRAPH),
"function graph ops unregistered without registering"))
- return;
+ return false;
guard(mutex)(&ftrace_lock);
@@ -1440,7 +1453,8 @@ void unregister_ftrace_graph(struct fgraph_ops *gops)
if (fgraph_lru_release_index(gops->idx) < 0)
goto out;
- fgraph_array[gops->idx] = &fgraph_stub;
+ WRITE_ONCE(fgraph_array[gops->idx], &fgraph_stub);
+ removed = true;
ftrace_graph_active--;
@@ -1460,7 +1474,32 @@ void unregister_ftrace_graph(struct fgraph_ops *gops)
unregister_pm_notifier(&ftrace_suspend_notifier);
unregister_trace_sched_switch(ftrace_graph_probe_sched_switch, NULL);
}
- gops->saved_func = NULL;
out:
gops->ops.flags &= ~FTRACE_OPS_FL_GRAPH;
+ return removed;
+}
+
+/**
+ * unregister_ftrace_graph_finish - wait for readers of an unpublished operation
+ * @gops: graph operation previously unpublished by unregister_ftrace_graph_start()
+ *
+ * Wait for existing graph readers, then clear the saved callback. This may
+ * sleep. The caller may release @gops and its filter after this returns.
+ */
+void unregister_ftrace_graph_finish(struct fgraph_ops *gops)
+{
+ /*
+ * The graph callbacks run with preemption disabled and may run while
+ * RCU is not watching. Wait for both kinds of readers before the
+ * caller can release the graph ops or its filter.
+ */
+ synchronize_rcu_tasks_rude();
+ synchronize_rcu_tasks();
+ gops->saved_func = NULL;
+}
+
+void unregister_ftrace_graph(struct fgraph_ops *gops)
+{
+ if (unregister_ftrace_graph_start(gops))
+ unregister_ftrace_graph_finish(gops);
}
diff --git a/kernel/trace/fprobe.c b/kernel/trace/fprobe.c
index da286619c..c53ad37a0 100644
--- a/kernel/trace/fprobe.c
+++ b/kernel/trace/fprobe.c
@@ -14,9 +14,11 @@
#include <linux/rhashtable.h>
#include <linux/slab.h>
#include <linux/sort.h>
+#include <linux/workqueue.h>
#include <asm/fprobe.h>
+#include "ftrace_internal.h"
#include "trace.h"
#define FPROBE_IP_HASH_BITS 8
@@ -279,6 +281,14 @@ static struct fgraph_ops fprobe_graph_ops = {
static int nr_fgraph_fprobes;
/* Is fprobe_graph_ops registered? */
static bool fprobe_graph_registered;
+static void fprobe_graph_cleanup_workfn(struct work_struct *work);
+static DECLARE_WORK(fprobe_graph_cleanup_work, fprobe_graph_cleanup_workfn);
+
+static void fprobe_graph_cleanup_workfn(struct work_struct *work)
+{
+ unregister_ftrace_graph_finish(&fprobe_graph_ops);
+ ftrace_free_filter(&fprobe_graph_ops.ops);
+}
/* Add @addrs to the ftrace filter and register fgraph if needed. */
static int fprobe_graph_add_ips(unsigned long *addrs, int num)
@@ -286,6 +296,7 @@ static int fprobe_graph_add_ips(unsigned long *addrs, int num)
int ret;
lockdep_assert_held(&fprobe_mutex);
+ flush_work(&fprobe_graph_cleanup_work);
ret = ftrace_set_filter_ips(&fprobe_graph_ops.ops, addrs, num, 0, 0);
if (ret)
@@ -297,7 +308,7 @@ static int fprobe_graph_add_ips(unsigned long *addrs, int num)
ftrace_free_filter(&fprobe_graph_ops.ops);
return ret;
}
- fprobe_graph_registered = true;
+ WRITE_ONCE(fprobe_graph_registered, true);
}
return 0;
}
@@ -305,9 +316,9 @@ static int fprobe_graph_add_ips(unsigned long *addrs, int num)
static void __fprobe_graph_unregister(void)
{
if (fprobe_graph_registered) {
- unregister_ftrace_graph(&fprobe_graph_ops);
- ftrace_free_filter(&fprobe_graph_ops.ops);
- fprobe_graph_registered = false;
+ WRITE_ONCE(fprobe_graph_registered, false);
+ if (unregister_ftrace_graph_start(&fprobe_graph_ops))
+ schedule_work(&fprobe_graph_cleanup_work);
}
}
@@ -679,6 +690,8 @@ static void fprobe_return(struct ftrace_graph_ret *trace,
ret_ip = ftrace_regs_get_instruction_pointer(fregs);
guard(rcu_sched_notrace)();
+ if (!READ_ONCE(fprobe_graph_registered))
+ return;
curr = 0;
while (size_words > curr) {
diff --git a/kernel/trace/ftrace_internal.h b/kernel/trace/ftrace_internal.h
index 3235470e6..abb7776a2 100644
--- a/kernel/trace/ftrace_internal.h
+++ b/kernel/trace/ftrace_internal.h
@@ -52,6 +52,8 @@ static inline int ftrace_shutdown_subops(struct ftrace_ops *ops, struct ftrace_o
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
extern int ftrace_graph_active;
+bool unregister_ftrace_graph_start(struct fgraph_ops *gops);
+void unregister_ftrace_graph_finish(struct fgraph_ops *gops);
# ifdef CONFIG_DYNAMIC_FTRACE
extern void fgraph_update_pid_func(void);
# else
--
2.56.0
^ permalink raw reply [flat|nested] 2+ messages in thread
* [PATCH 2/2] ftrace: Complete subops teardown after hash update failure
2026-11-03 17:58 [PATCH 1/2] ftrace: Wait for graph readers before releasing operations Weiming Shi
@ 2026-11-03 17:58 ` Weiming Shi
0 siblings, 0 replies; 2+ messages in thread
From: Weiming Shi @ 2026-11-03 17:58 UTC (permalink / raw)
To: Steven Rostedt, Masami Hiramatsu, Mark Rutland, Mathieu Desnoyers
Cc: linux-kernel, linux-trace-kernel, co+bba0c6b49150bef3, Xiang Mei,
Weiming Shi, stable
When removing a graph operation while other operations remain,
ftrace_shutdown_subops() rebuilds the manager hashes. Memory allocations
in rebuild_hashes() or ftrace_update_ops() can fail with -ENOMEM.
On a rebuild failure, the subop has been removed from the list but still
has FTRACE_OPS_FL_ENABLED and FTRACE_OPS_FL_SUBOP set. On an update
failure, it is added back to the list. Neither outcome matches
unregister_ftrace_graph(), which has already removed the graph slot.
For example, enable a function_graph tracer with a nonempty filter and a
return fprobe. Fail an allocation in rebuild_hashes() while disabling the
fprobe, disable the tracer, then re-enable the fprobe. The stale SUBOP
state routes its filter update through the now-empty manager list.
rebuild_hashes() leaves its output pointers uninitialized, and ops_equal()
can crash when dereferencing them.
Complete non-final subop removal on both error paths by clearing its
enabled/subop flags and manager pointer without adding it back to the
list. Initialize the rebuild outputs to EMPTY_HASH as well.
A failed manager hash update may leave functions enabled that only the
removed operation traced. Keep per-subop filtering in that case, since
direct graph dispatch skips those checks. Restore direct dispatch after
a successful removal from two graph operations to one, or after a
successful first registration.
This depends on the reader synchronization in the preceding patch.
Clearing the enabled flag after a failure allows the caller to release the
subop filter, while an existing graph callback may still hold it. Backport
both patches together.
Oops: general protection fault, probably for non-canonical address 0xdffffc0000000802: 0000 [#1] SMP KASAN NOPTI
KASAN: probably user-memory-access in range [0x0000000000004010-0x0000000000004017]
RIP: ops_equal (kernel/trace/trace.h:1011 kernel/trace/ftrace.c:3416)
Call Trace:
ftrace_update_ops (kernel/trace/ftrace.c:3464)
ftrace_hash_move_and_update_subops.isra.0 (kernel/trace/ftrace.c:3795)
ftrace_hash_move_and_update_ops (kernel/trace/ftrace.c:5095)
ftrace_set_hash (kernel/trace/ftrace.c:6006)
ftrace_set_filter_ips (kernel/trace/ftrace.c:6020 kernel/trace/ftrace.c:6769)
register_fprobe_ips (kernel/trace/fprobe.c:290 kernel/trace/fprobe.c:1020)
register_fprobe (kernel/trace/fprobe.c:979)
enable_trace_fprobe (kernel/trace/trace_fprobe.c:838 kernel/trace/trace_fprobe.c:1480)
fprobe_register (kernel/trace/trace_fprobe.c:1555)
__ftrace_event_enable_disable (kernel/trace/trace_events.c:913)
event_enable_write (kernel/trace/trace_events.c:943 kernel/trace/trace_events.c:1995)
vfs_write (fs/read_write.c:685)
ksys_write (fs/read_write.c:739)
do_syscall_64 (arch/x86/entry/syscall_64.c:61 arch/x86/entry/syscall_64.c:84)
entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121)
Kernel panic - not syncing: Fatal exception
Cc: stable@vger.kernel.org
Fixes: 0ae6b8ce200d ("ftrace: Fix accounting of subop hashes")
Reported-by: <co+bba0c6b49150bef3@bugs.sh>
Assisted-by: LLM
Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
kernel/trace/fgraph.c | 10 +++++++---
kernel/trace/ftrace.c | 22 ++++++++++++++--------
2 files changed, 21 insertions(+), 11 deletions(-)
diff --git a/kernel/trace/fgraph.c b/kernel/trace/fgraph.c
index 14733f519..48521bc33 100644
--- a/kernel/trace/fgraph.c
+++ b/kernel/trace/fgraph.c
@@ -1406,8 +1406,11 @@ int register_ftrace_graph(struct fgraph_ops *gops)
gops->ops.flags |= FTRACE_OPS_FL_GRAPH;
ret = ftrace_startup_subops(&graph_ops, &gops->ops, command);
- if (!ret)
+ if (!ret) {
fgraph_array[i] = gops;
+ if (ftrace_graph_active == 1)
+ ftrace_graph_enable_direct(true, gops);
+ }
error:
if (ret) {
@@ -1435,6 +1438,7 @@ int register_ftrace_graph(struct fgraph_ops *gops)
bool unregister_ftrace_graph_start(struct fgraph_ops *gops)
{
int command = 0;
+ int ret = -EINVAL;
bool removed = false;
if (WARN_ONCE(!(gops->ops.flags & FTRACE_OPS_FL_GRAPH),
@@ -1461,9 +1465,9 @@ bool unregister_ftrace_graph_start(struct fgraph_ops *gops)
if (!ftrace_graph_active)
command = FTRACE_STOP_FUNC_RET;
- ftrace_shutdown_subops(&graph_ops, &gops->ops, command);
+ ret = ftrace_shutdown_subops(&graph_ops, &gops->ops, command);
- if (ftrace_graph_active == 1)
+ if (ftrace_graph_active == 1 && !ret)
ftrace_graph_enable_direct(true, NULL);
else if (!ftrace_graph_active)
ftrace_graph_disable_direct(false);
diff --git a/kernel/trace/ftrace.c b/kernel/trace/ftrace.c
index c7d74b33d..7c712d99f 100644
--- a/kernel/trace/ftrace.c
+++ b/kernel/trace/ftrace.c
@@ -3672,6 +3672,8 @@ static int rebuild_hashes(struct ftrace_hash **filter_hash, struct ftrace_hash *
bool first = true;
int ret;
+ *filter_hash = EMPTY_HASH;
+ *notrace_hash = EMPTY_HASH;
temp_hash.filter_hash = EMPTY_HASH;
temp_hash.notrace_hash = EMPTY_HASH;
@@ -3713,7 +3715,13 @@ static int rebuild_hashes(struct ftrace_hash **filter_hash, struct ftrace_hash *
* will not affect functions that are being traced by other subops that
* still exist in @ops.
*
- * If the last subops is removed from @ops, then @ops is shutdown normally.
+ * If @subops is the last subop, @ops is shut down normally. Otherwise,
+ * @subops is detached even if the manager hash update fails. The manager
+ * hashes may be partially updated, but still cover all functions required by
+ * the remaining subops.
+ *
+ * Return: 0 on success, or a negative error code on shutdown or hash update
+ * failure.
*/
int ftrace_shutdown_subops(struct ftrace_ops *ops, struct ftrace_ops *subops, int command)
{
@@ -3753,15 +3761,13 @@ int ftrace_shutdown_subops(struct ftrace_ops *ops, struct ftrace_ops *subops, in
/* Rebuild the hashes without subops */
ret = rebuild_hashes(&filter_hash, ¬race_hash, ops);
if (ret < 0)
- return ret;
+ goto out;
ret = ftrace_update_ops(ops, filter_hash, notrace_hash);
- if (ret < 0) {
- list_add(&subops->list, &ops->subop_list);
- } else {
- subops->flags &= ~(FTRACE_OPS_FL_ENABLED | FTRACE_OPS_FL_SUBOP);
- subops->managed = NULL;
- }
+
+out:
+ subops->flags &= ~(FTRACE_OPS_FL_ENABLED | FTRACE_OPS_FL_SUBOP);
+ subops->managed = NULL;
free_ftrace_hash(filter_hash);
free_ftrace_hash(notrace_hash);
return ret;
--
2.56.0
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-11-03 17:58 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-03 17:58 [PATCH 1/2] ftrace: Wait for graph readers before releasing operations Weiming Shi
2026-11-03 17:58 ` [PATCH 2/2] ftrace: Complete subops teardown after hash update failure Weiming Shi
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®