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