* [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
* 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
* [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 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