[PATCH bpf-next 2/2] selftests/bpf: port test_ftrace.sh to test_progs

From: Alexis Lothoré (eBPF Foundation)

Date: Fri Oct 30 2026 - 14:51:53 EST


Commit ff205766dbbe ("ftrace: Fix function_graph tracer interaction with
BPF trampoline") made sure that trampolines generated for tracing
programs did not collide with the function graph when we are returning
from the hooked function. The series bringing this commit also came with
a new test ensuring that the kernel does not crash when executing BPF
programs handled by trampolines while the function graph trace is
enabled, but this test is a standalone shell script, hence not executed
in CI.

Convert the script to the test_progs runner so it can be executed
automatically by the CI infrastructure. There are two main options to
port this test:
- just enable ftrace during some existing trampoline-related tests
(eg: fentry/fexit tests)
- create a dedicated, minimal test with function graph

The new test adopts the latter approach, with a smaller scope to keep
execution time reasonable. On top of some simple fentry/fexit program,
it also adds a small fmod_ret program to ensure that return values are
correctly handled while the function graph enabled. Despite the few
asserts in the new test, the actual goal is to make sure that the
kernel does not crash because of some incorrect stack frame skipping
when both the function graph and tracing programs are running.

ftrace is configured through the new ftrace helpers, and restored to its
original state afterward. Despite being restored afterward, it is
configured for the whole system, so the test is marked as serial_ to
avoid conflicts with other tests needing traces (eg trace_vprintk)

With the new test integrated into test_progs, drop test_ftrace.sh.

Signed-off-by: Alexis Lothoré (eBPF Foundation) <alexis.lothore@xxxxxxxxxxx>
---
.../selftests/bpf/prog_tests/fgraph_trampoline.c | 129 +++++++++++++++++++++
.../selftests/bpf/progs/fgraph_trampoline.c | 29 +++++
tools/testing/selftests/bpf/test_ftrace.sh | 44 -------
3 files changed, 158 insertions(+), 44 deletions(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/fgraph_trampoline.c b/tools/testing/selftests/bpf/prog_tests/fgraph_trampoline.c
new file mode 100644
index 000000000000..98edd77cf214
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/fgraph_trampoline.c
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <test_progs.h>
+#include "trace_helpers.h"
+#include "fgraph_trampoline.skel.h"
+
+#define TRACER_GRAPH "function_graph"
+
+#define FTRACE_NOTRACE "*printk* *console* *wake* *serial* *lock*"
+#define GRAPH_FUNCS "bpf_prog_test*"
+
+/* See test_run.c: we are overriding bpf_modify_return_test with a program
+ * returning 5, but not bpf_modify_return_test2
+ */
+#define FMOD_EXPECTED_RETVAL ((1 << 16) | (5 + 1 + 3 + 3 + 4 + 5 + 6 + 7))
+
+static int configure_tracer(struct tracefs_ctl *ftrace)
+{
+ int err;
+
+ err = tracefs_ctl_filter_write(ftrace, "set_ftrace_filter", "");
+ if (err)
+ return err;
+ err = tracefs_ctl_filter_write(ftrace, "set_ftrace_notrace",
+ FTRACE_NOTRACE);
+ if (err)
+ return err;
+ err = tracefs_ctl_filter_write(ftrace, "set_graph_function", GRAPH_FUNCS);
+ if (err)
+ return err;
+ err = tracefs_ctl_filter_write(ftrace, "set_graph_notrace", "");
+ if (err)
+ return err;
+ err = tracefs_ctl_set(ftrace, "current_tracer", TRACER_GRAPH);
+ if (err)
+ return err;
+
+ return err;
+}
+
+static int run_test_under_ftrace_graph(struct tracefs_ctl *ftrace,
+ struct bpf_program *prog, int *retval)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ int err, stop_err;
+
+ err = tracefs_ctl_set(ftrace, "tracing_on", "1");
+ if (err)
+ return err;
+
+ err = bpf_prog_test_run_opts(bpf_program__fd(prog), &opts);
+ if (retval)
+ *retval = opts.retval;
+
+ stop_err = tracefs_ctl_set(ftrace, "tracing_on", "0");
+ if (!err)
+ err = stop_err;
+
+ return err;
+}
+
+void serial_test_fgraph_trampoline(void)
+{
+ struct fgraph_trampoline *skel = NULL;
+ struct tracefs_ctl *ftrace;
+ int retval;
+
+ ftrace = tracefs_ctl_init();
+ if (!ASSERT_OK_PTR(ftrace, "tracefs_ctl_init"))
+ return;
+
+ if (configure_tracer(ftrace)) {
+ test__skip();
+ tracefs_ctl_restore(ftrace);
+ goto cleanup;
+ }
+
+ skel = fgraph_trampoline__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "fgraph_trampoline__open_and_load"))
+ goto cleanup;
+
+ if (test__start_subtest("no_trampoline")) {
+ ASSERT_OK(run_test_under_ftrace_graph(ftrace,
+ skel->progs.fentry_test1,
+ NULL),
+ "run test no trampoline");
+ }
+
+ if (test__start_subtest("attach_fentry_fexit")) {
+ skel->links.fentry_test1 =
+ bpf_program__attach(skel->progs.fentry_test1);
+ if (!ASSERT_OK_PTR(skel->links.fentry_test1, "attach_fentry"))
+ goto destroy_cleanup;
+ skel->links.fexit_test1 =
+ bpf_program__attach(skel->progs.fexit_test1);
+ if (!ASSERT_OK_PTR(skel->links.fexit_test1, "attach_fexit"))
+ goto destroy_cleanup;
+
+ memset(skel->bss, 0, sizeof(*skel->bss));
+ ASSERT_OK(run_test_under_ftrace_graph(ftrace,
+ skel->progs.fentry_test1,
+ NULL),
+ "run test with fentry and fexit");
+
+ ASSERT_EQ(skel->bss->fexit_result, 1, "fexit_result");
+
+ fgraph_trampoline__detach(skel);
+ }
+
+ if (test__start_subtest("attach_fmod_ret")) {
+ skel->links.fmod_ret_test =
+ bpf_program__attach(skel->progs.fmod_ret_test);
+ if (!ASSERT_OK_PTR(skel->links.fmod_ret_test, "attach_fmod_ret"))
+ goto destroy_cleanup;
+
+ ASSERT_OK(run_test_under_ftrace_graph(ftrace,
+ skel->progs.fmod_ret_test,
+ &retval),
+ "run test with fmod_ret");
+ ASSERT_EQ(retval, FMOD_EXPECTED_RETVAL, "fmod return value");
+
+ fgraph_trampoline__detach(skel);
+ }
+
+destroy_cleanup:
+ fgraph_trampoline__destroy(skel);
+cleanup:
+ tracefs_ctl_restore(ftrace);
+}
diff --git a/tools/testing/selftests/bpf/progs/fgraph_trampoline.c b/tools/testing/selftests/bpf/progs/fgraph_trampoline.c
new file mode 100644
index 000000000000..1cc2dadc817e
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/fgraph_trampoline.c
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+__u64 fexit_result;
+
+SEC("fentry/bpf_fentry_test1")
+int BPF_PROG(fentry_test1, int a)
+{
+ return 0;
+}
+
+SEC("fexit/bpf_fentry_test1")
+int BPF_PROG(fexit_test1, int a, int ret)
+{
+ fexit_result = (a == 1 && ret == 2);
+ return 0;
+}
+
+SEC("fmod_ret/bpf_modify_return_test")
+int BPF_PROG(fmod_ret_test, int a, int *b, int ret)
+{
+ return 5;
+}
+
+char _license[] SEC("license") = "GPL";
+
diff --git a/tools/testing/selftests/bpf/test_ftrace.sh b/tools/testing/selftests/bpf/test_ftrace.sh
deleted file mode 100755
index f5109eb0e951..000000000000
--- a/tools/testing/selftests/bpf/test_ftrace.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/bin/bash
-
-if [[ -e /sys/kernel/tracing/trace ]]; then
- TR=/sys/kernel/tracing/
-else
- TR=/sys/kernel/debug/tracing/
-fi
-
-clear_trace() { # reset trace output
- echo > $TR/trace
-}
-
-disable_tracing() { # stop trace recording
- echo 0 > $TR/tracing_on
-}
-
-enable_tracing() { # start trace recording
- echo 1 > $TR/tracing_on
-}
-
-reset_tracer() { # reset the current tracer
- echo nop > $TR/current_tracer
-}
-
-disable_tracing
-clear_trace
-
-echo "" > $TR/set_ftrace_filter
-echo '*printk* *console* *wake* *serial* *lock*' > $TR/set_ftrace_notrace
-
-echo "bpf_prog_test*" > $TR/set_graph_function
-echo "" > $TR/set_graph_notrace
-
-echo function_graph > $TR/current_tracer
-
-enable_tracing
-./test_progs -t fentry
-./test_progs -t fexit
-disable_tracing
-clear_trace
-
-reset_tracer
-
-exit 0

--
2.56.0