mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH bpf-next 0/2] Reject callback subprogs invoke tailcall
@ 2026-07-11 10:47 Pu Lehui
  2026-07-11 10:47 ` [PATCH bpf-next 1/2] bpf: " Pu Lehui
  2026-07-11 10:47 ` [PATCH bpf-next 2/2] selftests/bpf: Add testcases for callback with tailcall Pu Lehui
  0 siblings, 2 replies; 7+ messages in thread
From: Pu Lehui @ 2026-07-11 10:47 UTC (permalink / raw)
  To: bpf, linux-kernel, Björn Töpel, Daniel Borkmann
  Cc: Alexei Starovoitov, Andrii Nakryiko, Eduard Zingerman,
	Kumar Kartikeya Dwivedi, Yonghong Song, Martin KaFai Lau,
	John Fastabend, Song Liu, Jiri Olsa, Emil Tsalapatis, Pu Lehui,
	Pu Lehui

Some JIT compilers, such as x86_64, rely on a register to pass the TCC.
When subprograms of synchronous callback invoke tailcall, C helpers
invoking bpf callback clobber this register, and the corrupted TCC may
bypass the TCC limit, leading to infinite tailcall.

Fix this by rejecting tailcall inside all subprogs of sync callback.
This also cleanly consolidates the existing async and exception callback
checks into a single unified `is_cb` check.

Pu Lehui (2):
  bpf: Reject callback subprogs invoke tailcall
  selftests/bpf: Add testcases for callback with tailcall

 kernel/bpf/verifier.c                         |   8 +-
 .../selftests/bpf/prog_tests/tailcalls.c      |   7 +
 .../selftests/bpf/progs/tailcall_callback.c   | 129 ++++++++++++++++++
 3 files changed, 138 insertions(+), 6 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/progs/tailcall_callback.c

-- 
2.34.1


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

* [PATCH bpf-next 1/2] bpf: Reject callback subprogs invoke tailcall
  2026-07-11 10:47 [PATCH bpf-next 0/2] Reject callback subprogs invoke tailcall Pu Lehui
@ 2026-07-11 10:47 ` Pu Lehui
  2026-07-11 11:33   ` bot+bpf-ci
  2026-07-13 22:09   ` Eduard Zingerman
  2026-07-11 10:47 ` [PATCH bpf-next 2/2] selftests/bpf: Add testcases for callback with tailcall Pu Lehui
  1 sibling, 2 replies; 7+ messages in thread
From: Pu Lehui @ 2026-07-11 10:47 UTC (permalink / raw)
  To: bpf, linux-kernel, Björn Töpel, Daniel Borkmann
  Cc: Alexei Starovoitov, Andrii Nakryiko, Eduard Zingerman,
	Kumar Kartikeya Dwivedi, Yonghong Song, Martin KaFai Lau,
	John Fastabend, Song Liu, Jiri Olsa, Emil Tsalapatis, Pu Lehui,
	Pu Lehui

From: Pu Lehui <pulehui@huawei.com>

Some JIT compilers, such as x86_64, rely on a register to pass the TCC.
When subprograms of synchronous callback invoke tailcall, C helpers
invoking bpf callback clobber this register, and the corrupted TCC may
bypass the TCC limit, leading to infinite tailcall.

Fix this by rejecting tailcall inside all subprogs of sync callback.
This also cleanly consolidates the existing async and exception callback
checks into a single unified `is_cb` check.

Reported-by: Sashiko <sashiko-bot@kernel.org>
Reported-by: Björn Töpel <bjorn@kernel.org>
Signed-off-by: Pu Lehui <pulehui@huawei.com>
---
 kernel/bpf/verifier.c | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 03e2202cca13..3f6c8b8fc04d 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -5258,10 +5258,6 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
 		if (verifier_bug_if(sidx < 0, env, "callee not found at insn %d", next_insn))
 			return -EFAULT;
 		if (subprog[sidx].is_async_cb) {
-			if (subprog[sidx].has_tail_call) {
-				verifier_bug(env, "subprog has tail_call and async cb");
-				return -EFAULT;
-			}
 			/* async callbacks don't increase bpf prog stack size unless called directly */
 			if (!bpf_pseudo_call(insn + i))
 				continue;
@@ -5302,8 +5298,8 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
 	 */
 	if (tail_call_reachable) {
 		for (tmp = idx; tmp >= 0; tmp = dinfo[tmp].caller) {
-			if (subprog[tmp].is_exception_cb) {
-				verbose(env, "cannot tail call within exception cb\n");
+			if (subprog[tmp].is_cb) {
+				verbose(env, "cannot tail call within callback\n");
 				return -EINVAL;
 			}
 			if (subprog[tmp].stack_arg_cnt) {
-- 
2.34.1


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

* [PATCH bpf-next 2/2] selftests/bpf: Add testcases for callback with tailcall
  2026-07-11 10:47 [PATCH bpf-next 0/2] Reject callback subprogs invoke tailcall Pu Lehui
  2026-07-11 10:47 ` [PATCH bpf-next 1/2] bpf: " Pu Lehui
@ 2026-07-11 10:47 ` Pu Lehui
  2026-07-13 22:12   ` Eduard Zingerman
  1 sibling, 1 reply; 7+ messages in thread
From: Pu Lehui @ 2026-07-11 10:47 UTC (permalink / raw)
  To: bpf, linux-kernel, Björn Töpel, Daniel Borkmann
  Cc: Alexei Starovoitov, Andrii Nakryiko, Eduard Zingerman,
	Kumar Kartikeya Dwivedi, Yonghong Song, Martin KaFai Lau,
	John Fastabend, Song Liu, Jiri Olsa, Emil Tsalapatis, Pu Lehui,
	Pu Lehui

From: Pu Lehui <pulehui@huawei.com>

Add 3 testcases for callback with tailcall.
1. callback directly invokes tailcall
2. callback->subprog->tailcall
3. callback->subprog0->subprog1->tailcall

Signed-off-by: Pu Lehui <pulehui@huawei.com>
---
 .../selftests/bpf/prog_tests/tailcalls.c      |   7 +
 .../selftests/bpf/progs/tailcall_callback.c   | 129 ++++++++++++++++++
 2 files changed, 136 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/progs/tailcall_callback.c

diff --git a/tools/testing/selftests/bpf/prog_tests/tailcalls.c b/tools/testing/selftests/bpf/prog_tests/tailcalls.c
index a5a226d0104c..c66037162da5 100644
--- a/tools/testing/selftests/bpf/prog_tests/tailcalls.c
+++ b/tools/testing/selftests/bpf/prog_tests/tailcalls.c
@@ -12,6 +12,7 @@
 #include "tailcall_cgrp_storage_no_storage.skel.h"
 #include "tailcall_cgrp_storage.skel.h"
 #include "tailcall_sleepable.skel.h"
+#include "tailcall_callback.skel.h"
 
 /* test_tailcall_1 checks basic functionality by patching multiple locations
  * in a single program for a single tail call slot with nop->jmp, jmp->nop
@@ -1901,6 +1902,11 @@ static void test_tailcall_sleepable(void)
 	tailcall_sleepable__destroy(skel);
 }
 
+static void test_tailcall_callback(void)
+{
+	RUN_TESTS(tailcall_callback);
+}
+
 void test_tailcalls(void)
 {
 	if (test__start_subtest("tailcall_1"))
@@ -1967,4 +1973,5 @@ void test_tailcalls(void)
 		test_tailcall_cgrp_storage_no_storage_leaf();
 	if (test__start_subtest("tailcall_cgrp_storage_no_storage_bridge"))
 		test_tailcall_cgrp_storage_no_storage_bridge();
+	test_tailcall_callback();
 }
diff --git a/tools/testing/selftests/bpf/progs/tailcall_callback.c b/tools/testing/selftests/bpf/progs/tailcall_callback.c
new file mode 100644
index 000000000000..504d8e7a6996
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/tailcall_callback.c
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_misc.h"
+#include "bpf_test_utils.h"
+
+int classifier_0(struct __sk_buff *skb);
+int classifier_1(struct __sk_buff *skb);
+
+struct {
+	__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
+	__uint(max_entries, 2);
+	__uint(key_size, sizeof(__u32));
+	__array(values, void (void));
+} jmp_table SEC(".maps") = {
+	.values = {
+		[0] = (void *) &classifier_0,
+		[1] = (void *) &classifier_1,
+	},
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_ARRAY);
+	__uint(max_entries, 1);
+	__uint(key_size, sizeof(__u32));
+	__uint(value_size, sizeof(__u32));
+} arraymap SEC(".maps");
+
+__auxiliary
+SEC("tc")
+int classifier_0(struct __sk_buff *skb)
+{
+	return 0;
+}
+
+static __noinline
+int subprog_tail1(struct __sk_buff *skb)
+{
+	int ret = 0;
+
+	bpf_tail_call_static(skb, &jmp_table, 1);
+	barrier_var(ret);
+	return ret;
+}
+
+__auxiliary
+SEC("tc")
+int classifier_1(struct __sk_buff *skb)
+{
+	int ret;
+
+	ret = subprog_tail1(skb);
+	__sink(ret);
+	return 0;
+}
+
+static __noinline
+int subprog_tail0(struct __sk_buff *skb)
+{
+	int ret;
+
+	ret = subprog_tail1(skb);
+	barrier_var(ret);
+	return ret;
+}
+
+static __noinline
+int callback_loop_1(int index, void **cb_ctx)
+{
+	int ret = 0;
+
+	bpf_tail_call_static(*cb_ctx, &jmp_table, 0);
+	barrier_var(ret);
+	return ret;
+}
+
+static __noinline
+int callback_loop_2(int index, void **cb_ctx)
+{
+	int ret;
+
+	ret = subprog_tail1(*cb_ctx);
+	barrier_var(ret);
+	return ret ? 1 : 0;
+}
+
+static __noinline
+int callback_for_each(void *map, __u32 *key, __u64 *val, void **cb_ctx)
+{
+	int ret;
+
+	ret = subprog_tail0(*cb_ctx);
+	barrier_var(ret);
+	return ret ? 1 : 0;
+}
+/* callback involving tail call directly is rejected */
+SEC("tc")
+__failure __msg("callback unexpected regs 1")
+int tailcall_direct_callback(struct __sk_buff *skb)
+{
+	clobber_regs_stack();
+
+	bpf_loop(1, callback_loop_1, &skb, 0);
+	return 0;
+}
+
+/* callback involving 1 subprog with tail call is rejected */
+SEC("tc")
+__failure __msg("cannot tail call within callback")
+int tailcall_bpf2bpf_callback_1(struct __sk_buff *skb)
+{
+	clobber_regs_stack();
+
+	bpf_loop(1, callback_loop_2, &skb, 0);
+	return 0;
+}
+
+/* callback involving 2 subprogs with tail call is rejected */
+SEC("tc")
+__failure __msg("cannot tail call within callback")
+int tailcall_bpf2bpf_callback_2(struct __sk_buff *skb)
+{
+	clobber_regs_stack();
+
+	bpf_for_each_map_elem(&arraymap, callback_for_each, &skb, 0);
+	return 0;
+}
+
+char __license[] SEC("license") = "GPL";
-- 
2.34.1


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

* Re: [PATCH bpf-next 1/2] bpf: Reject callback subprogs invoke tailcall
  2026-07-11 10:47 ` [PATCH bpf-next 1/2] bpf: " Pu Lehui
@ 2026-07-11 11:33   ` bot+bpf-ci
  2026-07-13 22:09   ` Eduard Zingerman
  1 sibling, 0 replies; 7+ messages in thread
From: bot+bpf-ci @ 2026-07-11 11:33 UTC (permalink / raw)
  To: pulehui, bpf, linux-kernel, bjorn, daniel
  Cc: ast, andrii, eddyz87, memxor, yonghong.song, martin.lau,
	john.fastabend, song, jolsa, emil, pulehui, pulehui, ast, andrii,
	daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 1350 bytes --]

>     bpf: Reject callback subprogs invoke tailcall
>
>     Some JIT compilers, such as x86_64, rely on a register to pass the TCC.
>     When subprograms of synchronous callback invoke tailcall, C helpers
>     invoking bpf callback clobber this register, and the corrupted TCC may
>     bypass the TCC limit, leading to infinite tailcall.
>
>     Fix this by rejecting tailcall inside all subprogs of sync callback.
>     This also cleanly consolidates the existing async and exception callback
>     checks into a single unified `is_cb` check.
>
>     Signed-off-by: Pu Lehui <pulehui@huawei.com>

This looks like a bug fix, but there is no Fixes: tag.

The change broadens the tail_call_reachable guard in
check_max_stack_depth_subprog() from the exception-only test to the
unified is_cb test:

    if (subprog[tmp].is_cb)

The narrower "cannot tail call within exception cb" guard it replaces was
added by commit b9ae0c9dd0ac, which only covered exception callbacks and
left synchronous callbacks unprotected. Should this carry:

  Fixes: b9ae0c9dd0ac ("bpf: Add support for custom exception callbacks")


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/29150078207

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

* Re: [PATCH bpf-next 1/2] bpf: Reject callback subprogs invoke tailcall
  2026-07-11 10:47 ` [PATCH bpf-next 1/2] bpf: " Pu Lehui
  2026-07-11 11:33   ` bot+bpf-ci
@ 2026-07-13 22:09   ` Eduard Zingerman
  1 sibling, 0 replies; 7+ messages in thread
From: Eduard Zingerman @ 2026-07-13 22:09 UTC (permalink / raw)
  To: Pu Lehui, bpf, linux-kernel, Björn Töpel, Daniel Borkmann
  Cc: Alexei Starovoitov, Andrii Nakryiko, Kumar Kartikeya Dwivedi,
	Yonghong Song, Martin KaFai Lau, John Fastabend, Song Liu,
	Jiri Olsa, Emil Tsalapatis, Pu Lehui

On Sat, 2026-07-11 at 10:47 +0000, Pu Lehui wrote:
> From: Pu Lehui <pulehui@huawei.com>
> 
> Some JIT compilers, such as x86_64, rely on a register to pass the TCC.
> When subprograms of synchronous callback invoke tailcall, C helpers
> invoking bpf callback clobber this register, and the corrupted TCC may
> bypass the TCC limit, leading to infinite tailcall.
> 
> Fix this by rejecting tailcall inside all subprogs of sync callback.
> This also cleanly consolidates the existing async and exception callback
> checks into a single unified `is_cb` check.
> 
> Reported-by: Sashiko <sashiko-bot@kernel.org>
> Reported-by: Björn Töpel <bjorn@kernel.org>
> Signed-off-by: Pu Lehui <pulehui@huawei.com>
> ---

Acked-by: Eduard Zingerman <eddyz87@gmail.com>

[...]

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

* Re: [PATCH bpf-next 2/2] selftests/bpf: Add testcases for callback with tailcall
  2026-07-11 10:47 ` [PATCH bpf-next 2/2] selftests/bpf: Add testcases for callback with tailcall Pu Lehui
@ 2026-07-13 22:12   ` Eduard Zingerman
  2026-07-14  2:01     ` Pu Lehui
  0 siblings, 1 reply; 7+ messages in thread
From: Eduard Zingerman @ 2026-07-13 22:12 UTC (permalink / raw)
  To: Pu Lehui, bpf, linux-kernel, Björn Töpel, Daniel Borkmann
  Cc: Alexei Starovoitov, Andrii Nakryiko, Kumar Kartikeya Dwivedi,
	Yonghong Song, Martin KaFai Lau, John Fastabend, Song Liu,
	Jiri Olsa, Emil Tsalapatis, Pu Lehui

On Sat, 2026-07-11 at 10:47 +0000, Pu Lehui wrote:

[...]

> diff --git a/tools/testing/selftests/bpf/prog_tests/tailcalls.c b/tools/testing/selftests/bpf/prog_tests/tailcalls.c
> index a5a226d0104c..c66037162da5 100644
> --- a/tools/testing/selftests/bpf/prog_tests/tailcalls.c
> +++ b/tools/testing/selftests/bpf/prog_tests/tailcalls.c

[...]

>  void test_tailcalls(void)
>  {
>  	if (test__start_subtest("tailcall_1"))
> @@ -1967,4 +1973,5 @@ void test_tailcalls(void)
>  		test_tailcall_cgrp_storage_no_storage_leaf();
>  	if (test__start_subtest("tailcall_cgrp_storage_no_storage_bridge"))
>  		test_tailcall_cgrp_storage_no_storage_bridge();
> +	test_tailcall_callback();

Please use test__start_subtest().

>  }
> diff --git a/tools/testing/selftests/bpf/progs/tailcall_callback.c b/tools/testing/selftests/bpf/progs/tailcall_callback.c
> new file mode 100644
> index 000000000000..504d8e7a6996
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/tailcall_callback.c

[...]

> +/* callback involving 1 subprog with tail call is rejected */
> +SEC("tc")
> +__failure __msg("cannot tail call within callback")
> +int tailcall_bpf2bpf_callback_1(struct __sk_buff *skb)
> +{
> +	clobber_regs_stack();
> +
> +	bpf_loop(1, callback_loop_2, &skb, 0);
> +	return 0;
> +}

I think having all three is a bit redundant,
let's settle on tailcall_bpf2bpf_callback_1?

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

* Re: [PATCH bpf-next 2/2] selftests/bpf: Add testcases for callback with tailcall
  2026-07-13 22:12   ` Eduard Zingerman
@ 2026-07-14  2:01     ` Pu Lehui
  0 siblings, 0 replies; 7+ messages in thread
From: Pu Lehui @ 2026-07-14  2:01 UTC (permalink / raw)
  To: Eduard Zingerman, bpf, linux-kernel, Björn Töpel,
	Daniel Borkmann
  Cc: Alexei Starovoitov, Andrii Nakryiko, Kumar Kartikeya Dwivedi,
	Yonghong Song, Martin KaFai Lau, John Fastabend, Song Liu,
	Jiri Olsa, Emil Tsalapatis, Pu Lehui

Hi Eduard,

Thanks for reviewing.

On 2026/7/14 6:12, Eduard Zingerman wrote:
> On Sat, 2026-07-11 at 10:47 +0000, Pu Lehui wrote:
> 
> [...]
> 
>> diff --git a/tools/testing/selftests/bpf/prog_tests/tailcalls.c b/tools/testing/selftests/bpf/prog_tests/tailcalls.c
>> index a5a226d0104c..c66037162da5 100644
>> --- a/tools/testing/selftests/bpf/prog_tests/tailcalls.c
>> +++ b/tools/testing/selftests/bpf/prog_tests/tailcalls.c
> 
> [...]
> 
>>   void test_tailcalls(void)
>>   {
>>   	if (test__start_subtest("tailcall_1"))
>> @@ -1967,4 +1973,5 @@ void test_tailcalls(void)
>>   		test_tailcall_cgrp_storage_no_storage_leaf();
>>   	if (test__start_subtest("tailcall_cgrp_storage_no_storage_bridge"))
>>   		test_tailcall_cgrp_storage_no_storage_bridge();
>> +	test_tailcall_callback();
> 
> Please use test__start_subtest().

I use `RUN_TESTS(tailcall_callback)` above, so it will parse subtest 
base on `__description(desc)` or test func_proto. Also, it will make 
sense to test only by `test_progs -a tailcalls/tailcall_bpf2bpf_callback`


But if use `test__start_subtest`, it will show double test items:

#480/37  tailcalls/tailcall_cgrp_storage_no_storage_bridge:OK
#480/38  tailcalls/test_tailcall_bpf2bpf_callback:OK <--
#480/39  tailcalls/tailcall_bpf2bpf_callback:OK <--
#480     tailcalls:OK
Summary: 1/39 PASSED, 0 SKIPPED, 0 FAILED

> 
>>   }
>> diff --git a/tools/testing/selftests/bpf/progs/tailcall_callback.c b/tools/testing/selftests/bpf/progs/tailcall_callback.c
>> new file mode 100644
>> index 000000000000..504d8e7a6996
>> --- /dev/null
>> +++ b/tools/testing/selftests/bpf/progs/tailcall_callback.c
> 
> [...]
> 
>> +/* callback involving 1 subprog with tail call is rejected */
>> +SEC("tc")
>> +__failure __msg("cannot tail call within callback")
>> +int tailcall_bpf2bpf_callback_1(struct __sk_buff *skb)
>> +{
>> +	clobber_regs_stack();
>> +
>> +	bpf_loop(1, callback_loop_2, &skb, 0);
>> +	return 0;
>> +}
> 
> I think having all three is a bit redundant,
> let's settle on tailcall_bpf2bpf_callback_1?

make sense to me


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

end of thread, other threads:[~2026-07-14  2:01 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-11 10:47 [PATCH bpf-next 0/2] Reject callback subprogs invoke tailcall Pu Lehui
2026-07-11 10:47 ` [PATCH bpf-next 1/2] bpf: " Pu Lehui
2026-07-11 11:33   ` bot+bpf-ci
2026-07-13 22:09   ` Eduard Zingerman
2026-07-11 10:47 ` [PATCH bpf-next 2/2] selftests/bpf: Add testcases for callback with tailcall Pu Lehui
2026-07-13 22:12   ` Eduard Zingerman
2026-07-14  2:01     ` Pu Lehui

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