BPF List
 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; 6+ 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] 6+ 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 10:57   ` sashiko-bot
  2026-07-11 11:33   ` bot+bpf-ci
  2026-07-11 10:47 ` [PATCH bpf-next 2/2] selftests/bpf: Add testcases for callback with tailcall Pu Lehui
  1 sibling, 2 replies; 6+ 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 related	[flat|nested] 6+ 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-11 10:54   ` sashiko-bot
  1 sibling, 1 reply; 6+ 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 related	[flat|nested] 6+ 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-11 10:54   ` sashiko-bot
  0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-07-11 10:54 UTC (permalink / raw)
  To: Pu Lehui; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The test `tailcall_direct_callback` intentionally expects a verifier bug message (`"callback unexpected regs 1"`), which triggers a kernel `WARN_ONCE`.
--

--- Patch [2]: [PATCH bpf-next 2/2] selftests/bpf: Add testcases for callback with tailcall ---
commit 037342732803eb60c98636ac87d004705531bc5d
Author: Pu Lehui <pulehui@huawei.com>

selftests/bpf: Add testcases for callback with tailcall

This patch adds three testcases for BPF callbacks interacting with tail calls.
It tests scenarios where a callback directly invokes a tail call, or where
subprograms invoked by the callback issue the tail call.

> 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 0000000000000..504d8e7a69969
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/tailcall_callback.c
[ ... ]
> +/* callback involving tail call directly is rejected */
> +SEC("tc")
> +__failure __msg("callback unexpected regs 1")
> +int tailcall_direct_callback(struct __sk_buff *skb)
> +{

[Severity: High]
Does expecting this specific failure message codify a kernel warning as a
passing testcase?

The message "callback unexpected regs 1" is emitted by verifier_bug() in
kernel/bpf/backtrack.c during precision backtracking, which intentionally
triggers a kernel WARN_ONCE. On systems configured with panic_on_warn=1,
such as syzbot or standard CI environments, wouldn't loading this test cause
a kernel panic?

If the verifier hits an internal state corruption (tracking r0 back into a
faked exit) and triggers verifier_bug() before it can gracefully reject the
program, does this indicate that the underlying issue with callbacks and tail
calls isn't fully fixed by the series?

> +	clobber_regs_stack();
> +
> +	bpf_loop(1, callback_loop_1, &skb, 0);
> +	return 0;
> +}

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

^ permalink raw reply	[flat|nested] 6+ 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 10:57   ` sashiko-bot
  2026-07-11 11:33   ` bot+bpf-ci
  1 sibling, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-07-11 10:57 UTC (permalink / raw)
  To: Pu Lehui; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Security / Logic Bypass: Direct tail calls in async and exception callbacks bypass the verifier restrictions.
- [Medium] Spurious Verifier Rejection (False Positive): Valid BPF programs combining tail calls and sync callbacks are incorrectly rejected.
--

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

bpf: Reject callback subprogs invoke tailcall

The commit rejects tailcalls inside all subprograms of sync callbacks to
prevent infinite tailcalls caused by register clobbering. It also cleanly
consolidates the existing async and exception callback checks into a single
unified validation.

> 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 check allow async and exception callbacks with direct
tail calls to bypass verifier restrictions?

Because tail_call_reachable is initialized to false at the start of
check_max_stack_depth_subprog(), a direct tail call inside an async
callback (when evaluated as a root subprog) does not trigger the logic
to set tail_call_reachable to true. 

By removing this explicit check, the new validation block below is entirely
bypassed. This seems to allow unprivileged users to bypass the Tail Call
Counter (TCC) limits, potentially leading to runaway infinite tail calls and
kernel hangs.

>  			/* 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: Medium]
Could this unified check spuriously reject valid BPF programs combining
tail calls and sync callbacks?

When the verifier descends into a new subprogram (like a sync callback):

		idx = sidx;
		if (!priv_stack_supported)
			subprog[idx].priv_stack_mode = NO_PRIV_STACK;

		if (subprog[idx].has_tail_call)
			tail_call_reachable = true;

It appears the tail_call_reachable state is not reset to false. If a BPF
program sequentially calls a tail-call-reachable subprogram, and subsequently
passes a synchronous callback to a helper (like bpf_loop), the
tail_call_reachable boolean state from the first call leaks into the
callback's DFS evaluation.

The verifier might then wrongly assume the callback itself is
tail-call-reachable, incorrectly rejecting legitimate programs with -EINVAL.

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

^ permalink raw reply	[flat|nested] 6+ 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 10:57   ` sashiko-bot
@ 2026-07-11 11:33   ` bot+bpf-ci
  1 sibling, 0 replies; 6+ 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] 6+ messages in thread

end of thread, other threads:[~2026-07-11 11:33 UTC | newest]

Thread overview: 6+ 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 10:57   ` sashiko-bot
2026-07-11 11:33   ` bot+bpf-ci
2026-07-11 10:47 ` [PATCH bpf-next 2/2] selftests/bpf: Add testcases for callback with tailcall Pu Lehui
2026-07-11 10:54   ` sashiko-bot

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