Skip to contentSkip to conversation

Fix ftrace for livepatch + BPF fexit programs

PATCH Superseded Song Liu 5 participants 9 messages Oct 24, 2025 – Oct 26, 2025

Subsystems (8)
Stable branch Kselftest BPF Tracing +4 more subsystems

v2 of 4 Superseded Latest is v4 →

lore
Share & tools
lore

Song Liu

author maintainer
#0 lore raw

v2 cover Fix ftrace for livepatch + BPF fexit programs

livepatch and BPF trampoline are two special users of ftrace. livepatch uses ftrace with IPMODIFY flag and BPF trampoline uses ftrace direct functions. When livepatch and BPF trampoline with fexit programs attach to the same kernel function, BPF trampoline needs to call into the patched version of the kernel function.

1/3 and 2/3 of this patchset fix two issues with livepatch + fexit cases, one in the register_ftrace_direct path, the other in the modify_ftrace_direct path.

3/3 adds selftests for both cases.

---
Changes v1 => v2:
1. Target bpf tree. (Alexei)
2. Bring back the FTRACE_WARN_ON in __ftrace_hash_update_ipmodify for valid code paths. (Steven)
3. Update selftests with cleaner way to find livepatch-sample.ko. (offlline discussion with Ihor)

v1: https://lore.kernel.org/bpf/[email protected]/

Song Liu (3):
ftrace: Fix BPF fexit with livepatch
ftrace: bpf: Fix IPMODIFY + DIRECT in modify_ftrace_direct() selftests/bpf: Add tests for livepatch + bpf trampoline

 kernel/bpf/trampoline.c                       |  12 +-
 kernel/trace/ftrace.c                         |  42 +++++--
 tools/testing/selftests/bpf/config            |   3 +
 .../bpf/prog_tests/livepatch_trampoline.c     | 107 ++++++++++++++++++
 .../bpf/progs/livepatch_trampoline.c          |  30 +++++
 5 files changed, 180 insertions(+), 14 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/livepatch_trampoline.c
 create mode 100644 tools/testing/selftests/bpf/progs/livepatch_trampoline.c

--
2.47.3

author maintainer
#1 lore raw

PATCH v2 1/3 ftrace: Fix BPF fexit with livepatch

When livepatch is attached to the same function as bpf trampoline with a fexit program, bpf trampoline code calls register_ftrace_direct() twice. The first time will fail with -EAGAIN, and the second time it will succeed. This requires register_ftrace_direct() to unregister the address on the first attempt. Otherwise, the bpf trampoline cannot attach. Here is an easy way to reproduce this issue:

insmod samples/livepatch/livepatch-sample.ko
bpftrace -e 'fexit:cmdline_proc_show {}'
ERROR: Unable to attach probe: fexit:vmlinux:cmdline_proc_show...

Fix this by cleaning up the hash when register_ftrace_function_nolock hits errors.

Fixes d05cb470663a ("ftrace: Fix modification of direct_function hash while in use")
Cc Steven Rostedt (Google) <[email protected]>
Cc Masami Hiramatsu (Google) <[email protected]>
---
 kernel/trace/ftrace.c | 2 ++
 1 file changed, 2 insertions(+)
kernel/trace/ftrace.c +2 -0
index 42bd2ba68a82..7f432775a6b5 100644
@@ -6048,6 +6048,8 @@ int register_ftrace_direct(struct ftrace_ops *ops, unsigned long addr)
 	ops->direct_call = addr;
 
 	err = register_ftrace_function_nolock(ops);
+	if (err)
+		remove_direct_functions_hash(hash, addr);
 
  out_unlock:
 	mutex_unlock(&direct_mutex);
Signature · 1 line
2.47.3
author maintainer
#2 lore raw

PATCH v2 2/3 ftrace: bpf: Fix IPMODIFY + DIRECT in modify_ftrace_direct()

In reply to Song Liu

ftrace_hash_ipmodify_enable() checks IPMODIFY and DIRECT ftrace_ops on the same kernel function. When needed, ftrace_hash_ipmodify_enable() calls ops->ops_func() to prepare the direct ftrace (BPF trampoline) to share the same function as the IPMODIFY ftrace (livepatch).

ftrace_hash_ipmodify_enable() is called in register_ftrace_direct() path, but not called in modify_ftrace_direct() path. As a result, the following operations will break livepatch:

1. Load livepatch to a kernel function;
2. Attach fentry program to the kernel function;
3. Attach fexit program to the kernel function.

After 3, the kernel function being used will not be the livepatched version, but the original version.

Fix this by adding ftrace_hash_ipmodify_enable() to modify_ftrace_direct() and adjust some logic around the call.

---
 kernel/bpf/trampoline.c | 12 +++++++-----
 kernel/trace/ftrace.c   | 40 +++++++++++++++++++++++++++++++---------
 2 files changed, 38 insertions(+), 14 deletions(-)
kernel/bpf/trampoline.c +7 -5
index 5949095e51c3..023360568d02 100644
@@ -221,6 +221,13 @@ static int register_fentry(struct bpf_trampoline *tr, void *new_addr)
 
 	if (tr->func.ftrace_managed) {
 		ftrace_set_filter_ip(tr->fops, (unsigned long)ip, 0, 1);
+		/*
+		 * Clearing fops->trampoline and fops->NULL is
+		 * needed by the "goto again" case in
+		 * bpf_trampoline_update().
+		 */
+		tr->fops->trampoline = 0;
+		tr->fops->func = NULL;
 		ret = register_ftrace_direct(tr->fops, (long)new_addr);
 	} else {
 		ret = bpf_arch_text_poke(ip, BPF_MOD_CALL, NULL, new_addr);
@@ -479,11 +486,6 @@ static int bpf_trampoline_update(struct bpf_trampoline *tr, bool lock_direct_mut
 		 * BPF_TRAMP_F_SHARE_IPMODIFY is set, we can generate the
 		 * trampoline again, and retry register.
 		 */
-		/* reset fops->func and fops->trampoline for re-register */
-		tr->fops->func = NULL;
-		tr->fops->trampoline = 0;
-
-		/* free im memory and reallocate later */
 		bpf_tramp_image_free(im);
 		goto again;
 	}
kernel/trace/ftrace.c +31 -9
index 7f432775a6b5..0c91247a95ab 100644
@@ -1971,7 +1971,8 @@ static void ftrace_hash_rec_enable_modify(struct ftrace_ops *ops)
  */
 static int __ftrace_hash_update_ipmodify(struct ftrace_ops *ops,
 					 struct ftrace_hash *old_hash,
-					 struct ftrace_hash *new_hash)
+					 struct ftrace_hash *new_hash,
+					 bool update_target)
 {
 	struct ftrace_page *pg;
 	struct dyn_ftrace *rec, *end = NULL;
@@ -2006,10 +2007,13 @@ static int __ftrace_hash_update_ipmodify(struct ftrace_ops *ops,
 		if (rec->flags & FTRACE_FL_DISABLED)
 			continue;
 
-		/* We need to update only differences of filter_hash */
+		/*
+		 * Unless we are updating the target of a direct function,
+		 * we only need to update differences of filter_hash
+		 */
 		in_old = !!ftrace_lookup_ip(old_hash, rec->ip);
 		in_new = !!ftrace_lookup_ip(new_hash, rec->ip);
-		if (in_old == in_new)
+		if (!update_target && (in_old == in_new))
 			continue;
 
 		if (in_new) {
@@ -2020,7 +2024,16 @@ static int __ftrace_hash_update_ipmodify(struct ftrace_ops *ops,
 				if (is_ipmodify)
 					goto rollback;
 
-				FTRACE_WARN_ON(rec->flags & FTRACE_FL_DIRECT);
+				/*
+				 * If this is called by __modify_ftrace_direct()
+				 * then it is only chaning where the direct
+				 * pointer is jumping to, and the record already
+				 * points to a direct trampoline. If it isn't
+				 * then it is a bug to update ipmodify on a direct
+				 * caller.
+				 */
+				FTRACE_WARN_ON(!update_target &&
+					       (rec->flags & FTRACE_FL_DIRECT));
 
 				/*
 				 * Another ops with IPMODIFY is already
@@ -2076,7 +2089,7 @@ static int ftrace_hash_ipmodify_enable(struct ftrace_ops *ops)
 	if (ftrace_hash_empty(hash))
 		hash = NULL;
 
-	return __ftrace_hash_update_ipmodify(ops, EMPTY_HASH, hash);
+	return __ftrace_hash_update_ipmodify(ops, EMPTY_HASH, hash, false);
 }
 
 /* Disabling always succeeds */
@@ -2087,7 +2100,7 @@ static void ftrace_hash_ipmodify_disable(struct ftrace_ops *ops)
 	if (ftrace_hash_empty(hash))
 		hash = NULL;
 
-	__ftrace_hash_update_ipmodify(ops, hash, EMPTY_HASH);
+	__ftrace_hash_update_ipmodify(ops, hash, EMPTY_HASH, false);
 }
 
 static int ftrace_hash_ipmodify_update(struct ftrace_ops *ops,
@@ -2101,7 +2114,7 @@ static int ftrace_hash_ipmodify_update(struct ftrace_ops *ops,
 	if (ftrace_hash_empty(new_hash))
 		new_hash = NULL;
 
-	return __ftrace_hash_update_ipmodify(ops, old_hash, new_hash);
+	return __ftrace_hash_update_ipmodify(ops, old_hash, new_hash, false);
 }
 
 static void print_ip_ins(const char *fmt, const unsigned char *p)
@@ -6108,7 +6121,7 @@ EXPORT_SYMBOL_GPL(unregister_ftrace_direct);
 static int
 __modify_ftrace_direct(struct ftrace_ops *ops, unsigned long addr)
 {
-	struct ftrace_hash *hash;
+	struct ftrace_hash *hash = ops->func_hash->filter_hash;
 	struct ftrace_func_entry *entry, *iter;
 	static struct ftrace_ops tmp_ops = {
 		.func		= ftrace_stub,
@@ -6128,13 +6141,21 @@ __modify_ftrace_direct(struct ftrace_ops *ops, unsigned long addr)
 	if (err)
 		return err;
 
+	/*
+	 * Call __ftrace_hash_update_ipmodify() here, so that we can call
+	 * ops->ops_func for the ops. This is needed because the above
+	 * register_ftrace_function_nolock() worked on tmp_ops.
+	 */
+	err = __ftrace_hash_update_ipmodify(ops, hash, hash, true);
+	if (err)
+		goto out;
+
 	/*
 	 * Now the ftrace_ops_list_func() is called to do the direct callers.
 	 * We can safely change the direct functions attached to each entry.
 	 */
 	mutex_lock(&ftrace_lock);
 
-	hash = ops->func_hash->filter_hash;
 	size = 1 << hash->size_bits;
 	for (i = 0; i < size; i++) {
 		hlist_for_each_entry(iter, &hash->buckets[i], hlist) {
@@ -6149,6 +6170,7 @@ __modify_ftrace_direct(struct ftrace_ops *ops, unsigned long addr)
 
 	mutex_unlock(&ftrace_lock);
 
+out:
 	/* Removing the tmp_ops will add the updated direct callers to the functions */
 	unregister_ftrace_function(&tmp_ops);
 
Signature · 1 line
2.47.3
maintainer
#3 lore raw

Re: [PATCH v2 bpf 2/3] ftrace: bpf: Fix IPMODIFY + DIRECT in modify_ftrace_direct()

Quoted 6 lines · On Fri, 24 Oct 2025 11:29:00 -0700 Song Liu <[email protected]> wrote:
kernel/bpf/trampoline.c +0 -0
@@ -221,6 +221,13 @@ static int register_fentry(struct bpf_trampoline *tr, void *new_addr)
 
 	if (tr->func.ftrace_managed) {
 		ftrace_set_filter_ip(tr->fops, (unsigned long)ip, 0, 1);

kernel/bpf/trampoline.c static int register_fentry(struct bpf_trampoline *tr, void *new_addr) · line 233

Quoted 10 lines
kernel/bpf/trampoline.c +7 -0
+		/*
+		 * Clearing fops->trampoline and fops->NULL is
+		 * needed by the "goto again" case in
+		 * bpf_trampoline_update().
+		 */
+		tr->fops->trampoline = 0;
+		tr->fops->func = NULL;
 		ret = register_ftrace_direct(tr->fops, (long)new_addr);
 	} else {
 		ret = bpf_arch_text_poke(ip, BPF_MOD_CALL, NULL, new_addr);

I ran these through my own tests and this failed to build:

/work/build/trace/nobackup/linux-test.git/kernel/bpf/trampoline.c: In function ‘register_fentry’:
/work/build/trace/nobackup/linux-test.git/kernel/bpf/trampoline.c:229:25: error: invalid use of undefined type ‘struct ftrace_ops’
229 | tr->fops->trampoline = 0;
| ^~
/work/build/trace/nobackup/linux-test.git/kernel/bpf/trampoline.c:230:25: error: invalid use of undefined type ‘struct ftrace_ops’
230 | tr->fops->func = NULL;
| ^~
CC kernel/trace/rethook.o
make[5]: *** [/work/build/trace/nobackup/linux-test.git/scripts/Makefile.build:287: kernel/bpf/trampoline.o] Error 1 make[5]: *** Waiting for unfinished jobs....

Config attched.

-- Steve

author maintainer
#4 lore raw

Re: [PATCH v2 bpf 2/3] ftrace: bpf: Fix IPMODIFY + DIRECT in modify_ftrace_direct()

Quoted 34 lines · Steven Rostedt
Quoted 6 lines · On Fri, 24 Oct 2025 11:29:00 -0700 Song Liu <[email protected]> wrote:
kernel/bpf/trampoline.c +0 -0
@@ -221,6 +221,13 @@ static int register_fentry(struct bpf_trampoline *tr, void *new_addr)
 
      if (tr->func.ftrace_managed) {
              ftrace_set_filter_ip(tr->fops, (unsigned long)ip, 0, 1);

kernel/bpf/trampoline.c static int register_fentry(struct bpf_trampoline *tr, void *new_addr) · line 233

Quoted 10 lines
kernel/bpf/trampoline.c +7 -0
+             /*
+              * Clearing fops->trampoline and fops->NULL is
+              * needed by the "goto again" case in
+              * bpf_trampoline_update().
+              */
+             tr->fops->trampoline = 0;
+             tr->fops->func = NULL;
              ret = register_ftrace_direct(tr->fops, (long)new_addr);
      } else {
              ret = bpf_arch_text_poke(ip, BPF_MOD_CALL, NULL, new_addr);

I ran these through my own tests and this failed to build:

/work/build/trace/nobackup/linux-test.git/kernel/bpf/trampoline.c: In function ‘register_fentry’:
/work/build/trace/nobackup/linux-test.git/kernel/bpf/trampoline.c:229:25: error: invalid use of undefined type ‘struct ftrace_ops’
229 | tr->fops->trampoline = 0;
| ^~
/work/build/trace/nobackup/linux-test.git/kernel/bpf/trampoline.c:230:25: error: invalid use of undefined type ‘struct ftrace_ops’
230 | tr->fops->func = NULL;
| ^~
CC kernel/trace/rethook.o
make[5]: *** [/work/build/trace/nobackup/linux-test.git/scripts/Makefile.build:287: kernel/bpf/trampoline.o] Error 1 make[5]: *** Waiting for unfinished jobs....

Quoted 1 line

Config attched.

We need to guard this part with CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS. The suggestion by AI should also fix this, and give cleaner code.

Thanks,
Song

#5 lore raw

Re: [PATCH v2 bpf 2/3] ftrace: bpf: Fix IPMODIFY + DIRECT in modify_ftrace_direct()

In reply to Song Liu
kernel test robot noticed the following build errors:

Hi Song,

kernel test robot noticed the following build errors:

[auto build test ERROR on bpf/master]

url:    https://github.com/intel-lab-lkp/linux/commits/Song-Liu/ftrace-Fix-BPF-fexit-with-livepatch/20251025-023411
base:   https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf.git master
patch link:    https://lore.kernel.org/r/20251024182901.3247573-3-song%40kernel.org
patch subject: [PATCH v2 bpf 2/3] ftrace: bpf: Fix IPMODIFY + DIRECT in modify_ftrace_direct()
config: sparc-randconfig-r132-20251026 (https://download.01.org/0day-ci/archive/20251026/[email protected]/config)
compiler: sparc64-linux-gcc (GCC) 8.5.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20251026/[email protected]/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <[email protected]>
| Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/

All errors (new ones prefixed by >>):

kernel/bpf/trampoline.c: In function 'register_fentry':

>> kernel/bpf/trampoline.c:229:11: error: dereferencing pointer to incomplete type 'struct ftrace_ops'
      tr->fops->trampoline = 0;
              ^~

vim +229 kernel/bpf/trampoline.c

207
208 /* first time registering */
209 static int register_fentry(struct bpf_trampoline *tr, void *new_addr)
210 {
211 void *ip = tr->func.addr;
212 unsigned long faddr;
213 int ret;
214
215 faddr = ftrace_location((unsigned long)ip);
216 if (faddr) {
217 if (!tr->fops)
218 return -ENOTSUPP;
219 tr->func.ftrace_managed = true;
220 }
221
222 if (tr->func.ftrace_managed) {
223 ftrace_set_filter_ip(tr->fops, (unsigned long)ip, 0, 1);
224 /*
225 * Clearing fops->trampoline and fops->NULL is
226 * needed by the "goto again" case in
227 * bpf_trampoline_update().
228 */

 > 229			tr->fops->trampoline = 0;

230 tr->fops->func = NULL;
231 ret = register_ftrace_direct(tr->fops, (long)new_addr);
232 } else {
233 ret = bpf_arch_text_poke(ip, BPF_MOD_CALL, NULL, new_addr);
234 }
235
236 return ret;
237 }
238

Signature · 2 lines
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
author maintainer
#6 lore raw

PATCH v2 3/3 selftests/bpf: Add tests for livepatch + bpf trampoline

In reply to Song Liu

Both livepatch and BPF trampoline use ftrace. Special attention is needed when livepatch and fexit program touch the same function at the same time, because livepatch updates a kernel function and the BPF trampoline need to call into the right version of the kernel function.

Use samples/livepatch/livepatch-sample.ko for the test.

The test covers two cases:
1) When a fentry program is loaded first. This exercises the modify_ftrace_direct code path.
2) When a fentry program is loaded first. This exercises the register_ftrace_direct code path.

---
 tools/testing/selftests/bpf/config            |   3 +
 .../bpf/prog_tests/livepatch_trampoline.c     | 107 ++++++++++++++++++
 .../bpf/progs/livepatch_trampoline.c          |  30 +++++
 3 files changed, 140 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/livepatch_trampoline.c
 create mode 100644 tools/testing/selftests/bpf/progs/livepatch_trampoline.c
tools/testing/selftests/bpf/config +3 -0
index 70b28c1e653e..f2a2fd236ca8 100644
@@ -50,6 +50,7 @@ CONFIG_IPV6_SIT=y
 CONFIG_IPV6_TUNNEL=y
 CONFIG_KEYS=y
 CONFIG_LIRC=y
+CONFIG_LIVEPATCH=y
 CONFIG_LWTUNNEL=y
 CONFIG_MODULE_SIG=y
 CONFIG_MODULE_SRCVERSION_ALL=y
@@ -111,6 +112,8 @@ CONFIG_IP6_NF_FILTER=y
 CONFIG_NF_NAT=y
 CONFIG_PACKET=y
 CONFIG_RC_CORE=y
+CONFIG_SAMPLES=y
+CONFIG_SAMPLE_LIVEPATCH=m
 CONFIG_SECURITY=y
 CONFIG_SECURITYFS=y
 CONFIG_SYN_COOKIES=y
tools/testing/selftests/bpf/prog_tests/livepatch_trampoline.c new file +107 -0

107 lines not on this page · raw message

tools/testing/selftests/bpf/progs/livepatch_trampoline.c new file +30 -0
new file mode 100644
index 000000000000..15579d5bcd91
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
+
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+int fentry_hit;
+int fexit_hit;
+int my_pid;
+
+SEC("fentry/cmdline_proc_show")
+int BPF_PROG(fentry_cmdline)
+{
+	if (my_pid != (bpf_get_current_pid_tgid() >> 32))
+		return 0;
+
+	fentry_hit = 1;
+	return 0;
+}
+
+SEC("fexit/cmdline_proc_show")
+int BPF_PROG(fexit_cmdline)
+{
+	if (my_pid != (bpf_get_current_pid_tgid() >> 32))
+		return 0;
+
+	fexit_hit = 1;
+	return 0;
+}
Signature · 1 line
2.47.3
#7 lore raw
In reply to Song Liu
Quoted 10 lines · Song Liu

livepatch and BPF trampoline are two special users of ftrace. livepatch uses ftrace with IPMODIFY flag and BPF trampoline uses ftrace direct functions. When livepatch and BPF trampoline with fexit programs attach to the same kernel function, BPF trampoline needs to call into the patched version of the kernel function.

Quoted 5 lines

1/3 and 2/3 of this patchset fix two issues with livepatch + fexit cases, one in the register_ftrace_direct path, the other in the modify_ftrace_direct path.

3/3 adds selftests for both cases.

AI has a good point on this:

https://github.com/kernel-patches/bpf/pull/10088#issuecomment-3444465504

I will wait a bit more for human to chime in before sending v3 with the suggestion by AI.

Thanks,
Song

#8 lore raw
Quoted 14 lines · Song Liu
Quoted 14 lines · Song Liu

livepatch and BPF trampoline are two special users of ftrace. livepatch uses ftrace with IPMODIFY flag and BPF trampoline uses ftrace direct functions. When livepatch and BPF trampoline with fexit programs attach to the same kernel function, BPF trampoline needs to call into the patched version of the kernel function.

1/3 and 2/3 of this patchset fix two issues with livepatch + fexit cases, one in the register_ftrace_direct path, the other in the modify_ftrace_direct path.

Quoted 5 lines
Quoted 1 line

3/3 adds selftests for both cases.

AI has a good point on this:

https://github.com/kernel-patches/bpf/pull/10088#issuecomment-3444465504

makes sense, I think it'll make the code easier to read

jirka

Quoted 8 lines

I will wait a bit more for human to chime in before sending v3 with the suggestion by AI.

Thanks,
Song