Skip to contentSkip to conversation

tracing: fprobe: Protect return handler from recursion loop

PATCH In review Masami Hiramatsu (Google) 4 participants 9 messages Sep 22, 2025 – Sep 29, 2025

Subsystems (3)
lore
Share & tools
lore

Masami Hiramatsu (Google)

author maintainer
#0 lore raw

v2 cover tracing: fprobe: Protect return handler from recursion loop

The following series fixes a recursive loop issue on fprobe by adding ftrace_test_recursion_trylock() in __ftrace_return_to_handler(). The previous version is here;

https://lore.kernel.org/all/175828305637.117978.4183947592750468265.stgit@devnote2/

This v2 removes WARN_ON from __ftrace_return_to_handler() and adds kunit test cases for recursive call [2/2].

---

Masami Hiramatsu (Google) (2):
tracing: fgraph: Protect return handler from recursion loop lib/test_fprobe: Add recursion check test cases

 kernel/trace/fgraph.c   |   12 ++++++
 lib/tests/test_fprobe.c |   92 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 102 insertions(+), 2 deletions(-)

--
Masami Hiramatsu (Google) <[email protected]>

author maintainer
#1 lore raw

PATCH v2 1/2 tracing: fgraph: Protect return handler from recursion loop

From: Masami Hiramatsu (Google) <[email protected]>

function_graph_enter_regs() prevents itself from recursion by ftrace_test_recursion_trylock(), but __ftrace_return_to_handler(), which is called at the exit, does not prevent such recursion. Therefore, while it can prevent recursive calls from fgraph_ops::entryfunc(), it is not able to prevent recursive calls to fgraph from fgraph_ops::retfunc(), resulting in a recursive loop. This can lead an unexpected recursion bug reported by Menglong.

is_endbr() is called in __ftrace_return_to_handler -> fprobe_return
-> kprobe_multi_link_exit_handler -> is_endbr.

To fix this issue, acquire ftrace_test_recursion_trylock() in the __ftrace_return_to_handler() after unwind the shadow stack to mark this section must prevent recursive call of fgraph inside user-defined fgraph_ops::retfunc().

This is essentially a fix to commit 4346ba160409 ("fprobe: Rewrite fprobe on function-graph tracer"), because before that fgraph was only used from the function graph tracer. Fprobe allowed user to run any callbacks from fgraph after that commit.

---
Changes in v2:
- Do not warn on failing ftrace_test_recursion_trylock() because it allows one-level nest.

---
 kernel/trace/fgraph.c |   12 ++++++++++++
 1 file changed, 12 insertions(+)
kernel/trace/fgraph.c +12 -0
index 1e3b32b1e82c..484ad7a18463 100644
@@ -815,6 +815,7 @@ __ftrace_return_to_handler(struct ftrace_regs *fregs, unsigned long frame_pointe
 	unsigned long bitmap;
 	unsigned long ret;
 	int offset;
+	int bit;
 	int i;
 
 	ret_stack = ftrace_pop_return_trace(&trace, &ret, frame_pointer, &offset);
@@ -829,6 +830,15 @@ __ftrace_return_to_handler(struct ftrace_regs *fregs, unsigned long frame_pointe
 	if (fregs)
 		ftrace_regs_set_instruction_pointer(fregs, ret);
 
+	bit = ftrace_test_recursion_trylock(trace.func, ret);
+	/*
+	 * This can fail because ftrace_test_recursion_trylock() allows one nest
+	 * call. If we are already in a nested call, then we don't probe this and
+	 * just return the original return address.
+	 */
+	if (unlikely(bit < 0))
+		goto out;
+
 #ifdef CONFIG_FUNCTION_GRAPH_RETVAL
 	trace.retval = ftrace_regs_get_return_value(fregs);
 #endif
@@ -852,6 +862,8 @@ __ftrace_return_to_handler(struct ftrace_regs *fregs, unsigned long frame_pointe
 		}
 	}
 
+	ftrace_test_recursion_unlock(bit);
+out:
 	/*
 	 * The ftrace_graph_return() may still access the current
 	 * ret_stack structure, we need to make sure the update of
Tested-by Menglong Dong Acked-by Menglong Dong on v2 1/2
#2 lore raw

Re: [PATCH v2 1/2] tracing: fgraph: Protect return handler from recursion loop

On 2025/9/22 14:35 Masami Hiramatsu (Google) <[email protected]> write:

Quoted 42 lines

From: Masami Hiramatsu (Google) <[email protected]>

function_graph_enter_regs() prevents itself from recursion by ftrace_test_recursion_trylock(), but __ftrace_return_to_handler(), which is called at the exit, does not prevent such recursion. Therefore, while it can prevent recursive calls from fgraph_ops::entryfunc(), it is not able to prevent recursive calls to fgraph from fgraph_ops::retfunc(), resulting in a recursive loop. This can lead an unexpected recursion bug reported by Menglong.

is_endbr() is called in __ftrace_return_to_handler -> fprobe_return
-> kprobe_multi_link_exit_handler -> is_endbr.

To fix this issue, acquire ftrace_test_recursion_trylock() in the __ftrace_return_to_handler() after unwind the shadow stack to mark this section must prevent recursive call of fgraph inside user-defined fgraph_ops::retfunc().

This is essentially a fix to commit 4346ba160409 ("fprobe: Rewrite fprobe on function-graph tracer"), because before that fgraph was only used from the function graph tracer. Fprobe allowed user to run any callbacks from fgraph after that commit.

---
Changes in v2:
- Do not warn on failing ftrace_test_recursion_trylock() because it allows one-level nest.

---
 kernel/trace/fgraph.c |   12 ++++++++++++
 1 file changed, 12 insertions(+)
kernel/trace/fgraph.c +0 -0
index 1e3b32b1e82c..484ad7a18463 100644

kernel/trace/fgraph.c __ftrace_return_to_handler(struct ftrace_regs *fregs, unsigned long frame_pointe · line 819

Quoted 5 lines
kernel/trace/fgraph.c +1 -0
@@ -815,6 +815,7 @@ __ftrace_return_to_handler(struct ftrace_regs *fregs, unsigned long frame_pointe
 	unsigned long bitmap;
 	unsigned long ret;
 	int offset;
+	int bit;
 	int i;

The bpf bench testings work fine on this version :)

Thanks!
Menglong Dong

Quoted 30 lines
kernel/trace/fgraph.c +11 -0
 	ret_stack = ftrace_pop_return_trace(&trace, &ret, frame_pointer, &offset);
@@ -829,6 +830,15 @@ __ftrace_return_to_handler(struct ftrace_regs *fregs, unsigned long frame_pointe
 	if (fregs)
 		ftrace_regs_set_instruction_pointer(fregs, ret);
 
+	bit = ftrace_test_recursion_trylock(trace.func, ret);
+	/*
+	 * This can fail because ftrace_test_recursion_trylock() allows one nest
+	 * call. If we are already in a nested call, then we don't probe this and
+	 * just return the original return address.
+	 */
+	if (unlikely(bit < 0))
+		goto out;
+
 #ifdef CONFIG_FUNCTION_GRAPH_RETVAL
 	trace.retval = ftrace_regs_get_return_value(fregs);
 #endif
@@ -852,6 +862,8 @@ __ftrace_return_to_handler(struct ftrace_regs *fregs, unsigned long frame_pointe
 		}
 	}
 
+	ftrace_test_recursion_unlock(bit);
+out:
 	/*
 	 * The ftrace_graph_return() may still access the current
 	 * ret_stack structure, we need to make sure the update of
author maintainer
#3 lore raw

Re: [PATCH v2 1/2] tracing: fgraph: Protect return handler from recursion loop

In reply to Masami Hiramatsu (Google)

Hi Steve,

Can you pick this ? Or I will do?

Thanks,

Quoted 75 lines · On Mon, 22 Sep 2025 15:35:22 +0900 "Masami Hiramatsu (Google)"…

From: Masami Hiramatsu (Google) <[email protected]>

function_graph_enter_regs() prevents itself from recursion by ftrace_test_recursion_trylock(), but __ftrace_return_to_handler(), which is called at the exit, does not prevent such recursion. Therefore, while it can prevent recursive calls from fgraph_ops::entryfunc(), it is not able to prevent recursive calls to fgraph from fgraph_ops::retfunc(), resulting in a recursive loop. This can lead an unexpected recursion bug reported by Menglong.

is_endbr() is called in __ftrace_return_to_handler -> fprobe_return
-> kprobe_multi_link_exit_handler -> is_endbr.

To fix this issue, acquire ftrace_test_recursion_trylock() in the __ftrace_return_to_handler() after unwind the shadow stack to mark this section must prevent recursive call of fgraph inside user-defined fgraph_ops::retfunc().

This is essentially a fix to commit 4346ba160409 ("fprobe: Rewrite fprobe on function-graph tracer"), because before that fgraph was only used from the function graph tracer. Fprobe allowed user to run any callbacks from fgraph after that commit.

---
Changes in v2:
- Do not warn on failing ftrace_test_recursion_trylock() because it allows one-level nest.

---
 kernel/trace/fgraph.c |   12 ++++++++++++
 1 file changed, 12 insertions(+)
kernel/trace/fgraph.c +12 -0
index 1e3b32b1e82c..484ad7a18463 100644
@@ -815,6 +815,7 @@ __ftrace_return_to_handler(struct ftrace_regs *fregs, unsigned long frame_pointe
 	unsigned long bitmap;
 	unsigned long ret;
 	int offset;
+	int bit;
 	int i;
 
 	ret_stack = ftrace_pop_return_trace(&trace, &ret, frame_pointer, &offset);
@@ -829,6 +830,15 @@ __ftrace_return_to_handler(struct ftrace_regs *fregs, unsigned long frame_pointe
 	if (fregs)
 		ftrace_regs_set_instruction_pointer(fregs, ret);
 
+	bit = ftrace_test_recursion_trylock(trace.func, ret);
+	/*
+	 * This can fail because ftrace_test_recursion_trylock() allows one nest
+	 * call. If we are already in a nested call, then we don't probe this and
+	 * just return the original return address.
+	 */
+	if (unlikely(bit < 0))
+		goto out;
+
 #ifdef CONFIG_FUNCTION_GRAPH_RETVAL
 	trace.retval = ftrace_regs_get_return_value(fregs);
 #endif
@@ -852,6 +862,8 @@ __ftrace_return_to_handler(struct ftrace_regs *fregs, unsigned long frame_pointe
 		}
 	}
 
+	ftrace_test_recursion_unlock(bit);
+out:
 	/*
 	 * The ftrace_graph_return() may still access the current
 	 * ret_stack structure, we need to make sure the update of
Signature · 1 line
Masami Hiramatsu (Google) <[email protected]>
#4 lore raw

Re: [PATCH v2 1/2] tracing: fgraph: Protect return handler from recursion loop

Quoted 3 lines · On Thu, 25 Sep 2025 00:34:10 +0900 Masami Hiramatsu (Google)…

Hi Steve,

Can you pick this ? Or I will do?

I'll take it. I'm currently pulling in patches now anyway. Although, while I'm traveling, I'm a bit slow at it.

Thanks,

-- Steve

maintainer
#5 lore raw

Re: [PATCH v2 1/2] tracing: fgraph: Protect return handler from recursion loop

Quoted 10 lines · On Thu, 25 Sep 2025 03:26:11 -0400 Steven Rostedt <[email protected]> wrote:
Quoted 3 lines · On Thu, 25 Sep 2025 00:34:10 +0900 Masami Hiramatsu (Google)…

Hi Steve,

Can you pick this ? Or I will do?

I'll take it. I'm currently pulling in patches now anyway. Although, while I'm traveling, I'm a bit slow at it.

Masami, you didn't include linux-trace-kernel mailing list, so it's not in patchwork.

Can you please resend?

-- Steve

maintainer
#6 lore raw

Re: [PATCH v2 1/2] tracing: fgraph: Protect return handler from recursion loop

Quoted 3 lines · On Sat, 27 Sep 2025 08:57:53 -0400 Steven Rostedt <[email protected]> wrote:

Masami, you didn't include linux-trace-kernel mailing list, so it's not in patchwork.

Can you please resend?

Never mind, I sent it to Linus already.

-- Steve

author maintainer
#7 lore raw

Re: [PATCH v2 1/2] tracing: fgraph: Protect return handler from recursion loop

Quoted 5 lines · On Sun, 28 Sep 2025 08:56:26 -0400 Steven Rostedt <[email protected]> wrote:
Quoted 2 lines · On Sat, 27 Sep 2025 08:57:53 -0400 Steven Rostedt <[email protected]> wrote:

Masami, you didn't include linux-trace-kernel mailing list, so it's not in patchwork.

Oops, that is why it did not work...

Quoted 3 lines
Quoted 1 line

Can you please resend?

Never mind, I sent it to Linus already.

Thanks!

Quoted 2 lines

-- Steve

Signature · 1 line
Masami Hiramatsu (Google) <[email protected]>
author maintainer
#8 lore raw

PATCH v2 2/2 lib/test_fprobe: Add recursion check test cases

In reply to Masami Hiramatsu (Google)

From: Masami Hiramatsu (Google) <[email protected]>

Add test cases for checking recursion level must be less than 2. This ensures that fprobe can prevent infinite recursive call by itself.

---
 lib/tests/test_fprobe.c |   92 ++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 90 insertions(+), 2 deletions(-)
lib/tests/test_fprobe.c +90 -2
index cf92111b5c79..0022da242a83 100644
@@ -17,8 +17,10 @@ static u32 rand1, entry_val, exit_val;
 /* Use indirect calls to avoid inlining the target functions */
 static u32 (*target)(u32 value);
 static u32 (*target2)(u32 value);
+static u32 (*target3)(u32 value);
 static unsigned long target_ip;
 static unsigned long target2_ip;
+static unsigned long target3_ip;
 static int entry_return_value;
 
 static noinline u32 fprobe_selftest_target(u32 value)
@@ -31,13 +33,18 @@ static noinline u32 fprobe_selftest_target2(u32 value)
 	return (value / div_factor) + 1;
 }
 
+static noinline u32 fprobe_selftest_target3(u32 value)
+{
+	return (value / div_factor) + 2;
+}
+
 static notrace int fp_entry_handler(struct fprobe *fp, unsigned long ip,
 				    unsigned long ret_ip,
 				    struct ftrace_regs *fregs, void *data)
 {
 	KUNIT_EXPECT_FALSE(current_test, preemptible());
 	/* This can be called on the fprobe_selftest_target and the fprobe_selftest_target2 */
-	if (ip != target_ip)
+	if (ip != target_ip && ip != target3_ip)
 		KUNIT_EXPECT_EQ(current_test, ip, target2_ip);
 	entry_val = (rand1 / div_factor);
 	if (fp->entry_data_size) {
@@ -190,6 +197,84 @@ static void test_fprobe_skip(struct kunit *test)
 	KUNIT_EXPECT_EQ(test, 0, unregister_fprobe(&fp));
 }
 
+static int recur_count;
+static int recur_high;
+
+static notrace int fp_recur_entry_handler(struct fprobe *fp, unsigned long ip,
+					  unsigned long ret_ip,
+					  struct ftrace_regs *fregs, void *data)
+{
+	/* fgraph/fprobe allows one-level nesting. */
+	if (recur_count > 1)
+		return 0;
+
+	recur_count++;
+	if (recur_count > recur_high)
+		recur_high = recur_count;
+	target3(rand1);
+	recur_count--;
+
+	return 0;
+}
+
+static notrace void fp_recur_exit_handler(struct fprobe *fp, unsigned long ip,
+					  unsigned long ret_ip,
+					  struct ftrace_regs *fregs, void *data)
+{
+	/* fgraph/fprobe allows one-level nesting. */
+	if (recur_count > 1)
+		return;
+
+	recur_count++;
+	if (recur_count > recur_high)
+		recur_high = recur_count;
+	target3(rand1);
+	recur_count--;
+}
+
+static void test_fprobe_recursion_entry(struct kunit *test)
+{
+	struct fprobe fp = {
+		.entry_handler = fp_recur_entry_handler,
+	};
+	struct fprobe fp3 = {
+		.entry_handler = fp_recur_entry_handler,
+	};
+
+	current_test = test;
+	recur_count = 0;
+	recur_high = 0;
+	KUNIT_EXPECT_EQ(test, 0, register_fprobe(&fp, "fprobe_selftest_target", NULL));
+	KUNIT_EXPECT_EQ(test, 0, register_fprobe(&fp3, "fprobe_selftest_target3", NULL));
+
+	target(rand1);
+
+	KUNIT_EXPECT_LE(test, recur_high, 2);
+	KUNIT_EXPECT_EQ(test, 0, unregister_fprobe(&fp3));
+	KUNIT_EXPECT_EQ(test, 0, unregister_fprobe(&fp));
+}
+
+static void test_fprobe_recursion_exit(struct kunit *test)
+{
+	struct fprobe fp = {
+		.exit_handler = fp_recur_exit_handler,
+	};
+	struct fprobe fp3 = {
+		.exit_handler = fp_recur_exit_handler,
+	};
+
+	current_test = test;
+	recur_count = 0;
+	KUNIT_EXPECT_EQ(test, 0, register_fprobe(&fp, "fprobe_selftest_target", NULL));
+	KUNIT_EXPECT_EQ(test, 0, register_fprobe(&fp3, "fprobe_selftest_target3", NULL));
+
+	target(rand1);
+
+	KUNIT_EXPECT_LE(test, recur_high, 2);
+	KUNIT_EXPECT_EQ(test, 0, unregister_fprobe(&fp3));
+	KUNIT_EXPECT_EQ(test, 0, unregister_fprobe(&fp));
+}
+
 static unsigned long get_ftrace_location(void *func)
 {
 	unsigned long size, addr = (unsigned long)func;
@@ -205,8 +290,10 @@ static int fprobe_test_init(struct kunit *test)
 	rand1 = get_random_u32_above(div_factor);
 	target = fprobe_selftest_target;
 	target2 = fprobe_selftest_target2;
+	target3 = fprobe_selftest_target3;
 	target_ip = get_ftrace_location(target);
 	target2_ip = get_ftrace_location(target2);
+	target3_ip = get_ftrace_location(target3);
 
 	return 0;
 }
@@ -217,6 +304,8 @@ static struct kunit_case fprobe_testcases[] = {
 	KUNIT_CASE(test_fprobe_syms),
 	KUNIT_CASE(test_fprobe_data),
 	KUNIT_CASE(test_fprobe_skip),
+	KUNIT_CASE(test_fprobe_recursion_entry),
+	KUNIT_CASE(test_fprobe_recursion_exit),
 	{}
 };
 
@@ -227,4 +316,3 @@ static struct kunit_suite fprobe_test_suite = {
 };
 
 kunit_test_suites(&fprobe_test_suite);
-