* [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
* 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
* [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
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 a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox