All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH bpf-next v2 0/2] Reject callback subprogs invoke tailcall
@ 2026-07-14  2:44 Pu Lehui
  2026-07-14  2:44 ` [PATCH bpf-next v2 1/2] bpf: " Pu Lehui
  2026-07-14  2:44 ` [PATCH bpf-next v2 2/2] selftests/bpf: Add testcase for callback with tailcall Pu Lehui
  0 siblings, 2 replies; 6+ messages in thread
From: Pu Lehui @ 2026-07-14  2:44 UTC (permalink / raw)
  To: bpf, linux-kernel, Eduard Zingerman, Björn Töpel,
	Daniel Borkmann
  Cc: Alexei Starovoitov, Andrii Nakryiko, Kumar Kartikeya Dwivedi,
	Yonghong Song, Martin KaFai Lau, 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.

v2:
- remove redantant testcase.
- Add ACK tag by Eduard.

v1: https://lore.kernel.org/bpf/20260711104727.4023420-1-pulehui@huaweicloud.com

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

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

-- 
2.34.1


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

* [PATCH bpf-next v2 1/2] bpf: Reject callback subprogs invoke tailcall
  2026-07-14  2:44 [PATCH bpf-next v2 0/2] Reject callback subprogs invoke tailcall Pu Lehui
@ 2026-07-14  2:44 ` Pu Lehui
  2026-07-14  2:56   ` sashiko-bot
  2026-07-14  2:44 ` [PATCH bpf-next v2 2/2] selftests/bpf: Add testcase for callback with tailcall Pu Lehui
  1 sibling, 1 reply; 6+ messages in thread
From: Pu Lehui @ 2026-07-14  2:44 UTC (permalink / raw)
  To: bpf, linux-kernel, Eduard Zingerman, Björn Töpel,
	Daniel Borkmann
  Cc: Alexei Starovoitov, Andrii Nakryiko, Kumar Kartikeya Dwivedi,
	Yonghong Song, Martin KaFai Lau, 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>
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
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 related	[flat|nested] 6+ messages in thread

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

From: Pu Lehui <pulehui@huawei.com>

Add testcase for callback with tailcall, which is
callback->subprog->tailcall.

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

diff --git a/tools/testing/selftests/bpf/prog_tests/tailcalls.c b/tools/testing/selftests/bpf/prog_tests/tailcalls.c
index a5a226d0104c..6fbf4c37e234 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_bpf2bpf_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_bpf2bpf_callback(void)
+{
+	RUN_TESTS(tailcall_bpf2bpf_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_bpf2bpf_callback();
 }
diff --git a/tools/testing/selftests/bpf/progs/tailcall_bpf2bpf_callback.c b/tools/testing/selftests/bpf/progs/tailcall_bpf2bpf_callback.c
new file mode 100644
index 000000000000..dfe9c49cfeb1
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/tailcall_bpf2bpf_callback.c
@@ -0,0 +1,62 @@
+// 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);
+
+struct {
+	__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
+	__uint(max_entries, 1);
+	__uint(key_size, sizeof(__u32));
+	__array(values, void (void));
+} jmp_table SEC(".maps") = {
+	.values = {
+		[0] = (void *) &classifier_0,
+	},
+};
+
+static __noinline
+int subprog_tail0(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_0(struct __sk_buff *skb)
+{
+	int ret;
+
+	ret = subprog_tail0(skb);
+	__sink(ret);
+	return 0;
+}
+
+static __noinline
+int callback_loop(int index, void **cb_ctx)
+{
+	int ret;
+
+	ret = subprog_tail0(*cb_ctx);
+	barrier_var(ret);
+	return ret ? 1 : 0;
+}
+
+/* callback involving subprog with tail call is rejected */
+SEC("tc")
+__failure __msg("cannot tail call within callback")
+int tailcall_bpf2bpf_callback(struct __sk_buff *skb)
+{
+	clobber_regs_stack();
+
+	bpf_loop(1, callback_loop, &skb, 0);
+	return 0;
+}
+
+char __license[] SEC("license") = "GPL";
-- 
2.34.1


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

* Re: [PATCH bpf-next v2 2/2] selftests/bpf: Add testcase for callback with tailcall
  2026-07-14  2:44 ` [PATCH bpf-next v2 2/2] selftests/bpf: Add testcase for callback with tailcall Pu Lehui
@ 2026-07-14  2:53   ` sashiko-bot
  0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-07-14  2:53 UTC (permalink / raw)
  To: Pu Lehui; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Out-of-bounds map index used in bpf_tail_call_static in the test program.
--

--- Patch [2]: [PATCH bpf-next v2 2/2] selftests/bpf: Add testcase for callback with tailcall ---
commit 6b7ff517c87bc3ed869da7118100965c0668a781
Author: Pu Lehui <pulehui@huawei.com>

selftests/bpf: Add testcase for callback with tailcall

This commit adds a new BPF selftest to verify that a callback involving a
subprogram with a tail call is properly rejected by the verifier.

> diff --git a/tools/testing/selftests/bpf/progs/tailcall_bpf2bpf_callback.c b/tools/testing/selftests/bpf/progs/tailcall_bpf2bpf_callback.c
> new file mode 100644
> index 0000000000000..dfe9c49cfeb1d
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/tailcall_bpf2bpf_callback.c
> @@ -0,0 +1,62 @@
> +// 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);
> +
> +struct {
> +	__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
> +	__uint(max_entries, 1);
> +	__uint(key_size, sizeof(__u32));
> +	__array(values, void (void));
> +} jmp_table SEC(".maps") = {
> +	.values = {
> +		[0] = (void *) &classifier_0,
> +	},
> +};
> +
> +static __noinline
> +int subprog_tail0(struct __sk_buff *skb)
> +{
> +	int ret = 0;
> +
> +	bpf_tail_call_static(skb, &jmp_table, 1);

[Severity: Low]
Does this code use an out-of-bounds index for the tail call? The jmp_table
map has max_entries set to 1 and populates index 0 with classifier_0, but
this bpf_tail_call_static() invocation uses index 1.

Even though this test is designed to be rejected by the verifier, should
this pass index 0 to correctly match the intended tail call target?

> +	barrier_var(ret);
> +	return ret;
> +}

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260714024459.420075-1-pulehui@huaweicloud.com?part=2

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

* Re: [PATCH bpf-next v2 1/2] bpf: Reject callback subprogs invoke tailcall
  2026-07-14  2:44 ` [PATCH bpf-next v2 1/2] bpf: " Pu Lehui
@ 2026-07-14  2:56   ` sashiko-bot
  2026-07-14 13:47     ` Björn Töpel
  0 siblings, 1 reply; 6+ messages in thread
From: sashiko-bot @ 2026-07-14  2:56 UTC (permalink / raw)
  To: Pu Lehui; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] The patch inadvertently allows async and exception callbacks to execute tail calls, bypassing verifier protections and reintroducing the TCC register clobbering vulnerability.
- [High] The unified `is_cb` check introduces a DFS state leakage that incorrectly rejects valid BPF programs containing both a tail call and an unrelated synchronous callback.
--

--- Patch [1]: [PATCH bpf-next v2 1/2] bpf: Reject callback subprogs invoke tailcall ---
commit d260c3afbc5fedfa9c5c074686a31f057a21a53a
Author: Pu Lehui <pulehui@huawei.com>

bpf: Reject callback subprogs invoke tailcall

This patch rejects tailcalls inside all subprograms of synchronous callbacks
to prevent clobbering the TCC limit register on some JIT compilers. It also
consolidates async and exception callback checks into a single unified
is_cb check.

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 03e2202cca13d..3f6c8b8fc04d0 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;
> -			}

[Severity: High]
Does removing this explicit check allow async and exception callbacks to
execute tail calls when they contain no further subprogram calls?

Because async and exception callbacks are evaluated as the root of their own
call graphs by check_max_stack_depth_subprog() (with idx > 0), the local
tail_call_reachable variable remains false throughout the evaluation loop if
the callback makes no normal subprogram calls.

When the loop ends, the new unified is_cb check is bypassed because
tail_call_reachable is false, and the fallback check is skipped because !idx
is false for async callbacks. Can this bypass the TCC limit protections and
lead to infinite tailcalls?

>  			/* 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;
>  			}

[Severity: High]
Can the local tail_call_reachable variable leak across unrelated subprograms
during the verifier's Depth-First Search?

If a BPF program first calls a subprogram containing a tail call, the DFS
will set tail_call_reachable = true and mark it true for all ancestors.
When backtracking from the callee to the main program, tail_call_reachable
is restored to the ancestor's state:

kernel/bpf/verifier.c:check_max_stack_depth_subprog() {
    ...
	/* reset tail_call_reachable to the parent's actual state */
	tail_call_reachable = subprog[idx].tail_call_reachable;

	goto continue_func;
    ...
}

If the program subsequently sets up a synchronous callback (e.g., via
bpf_loop()), the loop jumps back up without resetting tail_call_reachable to
false. When it reaches the end of the callback subprogram,
tail_call_reachable will still be true from the leaked state.

Will this cause the new unified is_cb check to incorrectly reject perfectly
valid BPF programs that contain both a tail call and an unrelated synchronous
callback?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260714024459.420075-1-pulehui@huaweicloud.com?part=1

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

* Re: [PATCH bpf-next v2 1/2] bpf: Reject callback subprogs invoke tailcall
  2026-07-14  2:56   ` sashiko-bot
@ 2026-07-14 13:47     ` Björn Töpel
  0 siblings, 0 replies; 6+ messages in thread
From: Björn Töpel @ 2026-07-14 13:47 UTC (permalink / raw)
  To: sashiko-reviews, Pu Lehui; +Cc: bpf

sashiko-bot@kernel.org writes:

> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [High] The patch inadvertently allows async and exception callbacks to execute tail calls, bypassing verifier protections and reintroducing the TCC register clobbering vulnerability.
> - [High] The unified `is_cb` check introduces a DFS state leakage that incorrectly rejects valid BPF programs containing both a tail call and an unrelated synchronous callback.
> --
>
> --- Patch [1]: [PATCH bpf-next v2 1/2] bpf: Reject callback subprogs invoke tailcall ---
> commit d260c3afbc5fedfa9c5c074686a31f057a21a53a
> Author: Pu Lehui <pulehui@huawei.com>
>
> bpf: Reject callback subprogs invoke tailcall
>
> This patch rejects tailcalls inside all subprograms of synchronous callbacks
> to prevent clobbering the TCC limit register on some JIT compilers. It also
> consolidates async and exception callback checks into a single unified
> is_cb check.
>
>> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
>> index 03e2202cca13d..3f6c8b8fc04d0 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;
>> -			}
>
> [Severity: High]
> Does removing this explicit check allow async and exception callbacks to
> execute tail calls when they contain no further subprogram calls?
>
> Because async and exception callbacks are evaluated as the root of their own
> call graphs by check_max_stack_depth_subprog() (with idx > 0), the local
> tail_call_reachable variable remains false throughout the evaluation loop if
> the callback makes no normal subprogram calls.
>
> When the loop ends, the new unified is_cb check is bypassed because
> tail_call_reachable is false, and the fallback check is skipped because !idx
> is false for async callbacks. Can this bypass the TCC limit protections and
> lead to infinite tailcalls?
>
>>  			/* 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;
>>  			}
>
> [Severity: High]
> Can the local tail_call_reachable variable leak across unrelated subprograms
> during the verifier's Depth-First Search?
>
> If a BPF program first calls a subprogram containing a tail call, the DFS
> will set tail_call_reachable = true and mark it true for all ancestors.
> When backtracking from the callee to the main program, tail_call_reachable
> is restored to the ancestor's state:
>
> kernel/bpf/verifier.c:check_max_stack_depth_subprog() {
>     ...
> 	/* reset tail_call_reachable to the parent's actual state */
> 	tail_call_reachable = subprog[idx].tail_call_reachable;
>
> 	goto continue_func;
>     ...
> }
>
> If the program subsequently sets up a synchronous callback (e.g., via
> bpf_loop()), the loop jumps back up without resetting tail_call_reachable to
> false. When it reaches the end of the callback subprogram,
> tail_call_reachable will still be true from the leaked state.
>
> Will this cause the new unified is_cb check to incorrectly reject perfectly
> valid BPF programs that contain both a tail call and an unrelated synchronous
> callback?

Don't about "high severity", but this seems legit. It seems like we can
get "random" (depending of ordering) rejects of legit program, no? ...at
least the LLM I asked could construct a fix/PoC for it...


Björn


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

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

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-14  2:44 [PATCH bpf-next v2 0/2] Reject callback subprogs invoke tailcall Pu Lehui
2026-07-14  2:44 ` [PATCH bpf-next v2 1/2] bpf: " Pu Lehui
2026-07-14  2:56   ` sashiko-bot
2026-07-14 13:47     ` Björn Töpel
2026-07-14  2:44 ` [PATCH bpf-next v2 2/2] selftests/bpf: Add testcase for callback with tailcall Pu Lehui
2026-07-14  2:53   ` sashiko-bot

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.