All of lore.kernel.org
 help / color / mirror / Atom feed
From: Yonghong Song <yonghong.song@linux.dev>
To: Alexei Starovoitov <alexei.starovoitov@gmail.com>, bpf@vger.kernel.org
Cc: Alexei Starovoitov <ast@kernel.org>,
	Andrii Nakryiko <andrii@kernel.org>,
	Daniel Borkmann <daniel@iogearbox.net>,
	kernel-team@fb.com, Martin KaFai Lau <martin.lau@kernel.org>
Subject: Re: [PATCH bpf-next 1/3] selftests/bpf: Keep int return type for tailcall subprogs with maps
Date: Tue, 9 Jun 2026 14:30:34 -0700	[thread overview]
Message-ID: <145c7132-ec43-4238-801d-55d518fb0da8@linux.dev> (raw)
In-Reply-To: <DJ4PQYTQDWQT.1ENYNRIC3C7IH@gmail.com>



On 6/9/26 11:00 AM, Alexei Starovoitov wrote:
> On Tue Jun 9, 2026 at 9:39 AM PDT, Yonghong Song wrote:
>> LLVM23 ([1]) supports 'true' function signature in BTF. The return type
>> of the caller of a tailcall must be an 'int'. Otherwise, verification will
>> fail (see check_btf_func() in check_btf.c). So with llvm23, it is possible
>> that the compiler may change the caller's return type from 'int' to 'void'.
>> To prevent this, barrier_var() is used to avoid returning a constant prone
>> to be optimized.
>>
>> Use Array map to store the subprog return values. Otherwise, due to
>>
>>          data_map = bpf_object__find_map_by_name(fentry_obj, ".bss");
>>          if (!ASSERT_FALSE(!data_map || !bpf_map__is_internal(data_map),
>>                            "find data_map"))
>>                  goto out;
>>
>> in prog_tests/tailcalls.c test_tailcall_hierarchy_count(),
>> the test result will be different from the original one.
>>
>>    [1] https://github.com/llvm/llvm-project/pull/198426
>>
>> Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
>> ---
>>   .../bpf/progs/tailcall_bpf2bpf_hierarchy1.c   | 21 +++++++++++++++----
>>   .../progs/tailcall_bpf2bpf_hierarchy_fentry.c | 20 +++++++++++++++---
>>   2 files changed, 34 insertions(+), 7 deletions(-)
>>
>> diff --git a/tools/testing/selftests/bpf/progs/tailcall_bpf2bpf_hierarchy1.c b/tools/testing/selftests/bpf/progs/tailcall_bpf2bpf_hierarchy1.c
>> index d556b19413d7..13b0ee9475a8 100644
>> --- a/tools/testing/selftests/bpf/progs/tailcall_bpf2bpf_hierarchy1.c
>> +++ b/tools/testing/selftests/bpf/progs/tailcall_bpf2bpf_hierarchy1.c
>> @@ -11,25 +11,38 @@ struct {
>>   	__uint(value_size, sizeof(__u32));
>>   } jmp_table SEC(".maps");
>>   
>> +struct {
>> +	__uint(type, BPF_MAP_TYPE_ARRAY);
>> +	__uint(max_entries, 2);
>> +	__type(key, int);
>> +	__type(value, __u64);
>> +} array SEC(".maps");
>> +
>>   int count = 0;
>>   
>>   static __noinline
>>   int subprog_tail(struct __sk_buff *skb)
>>   {
>> +	int ret = 0;
>> +
>>   	bpf_tail_call_static(skb, &jmp_table, 0);
>> -	return 0;
>> +	barrier_var(ret);
>> +	return ret;
> I think it's missing the actual fix.
> bpf_tail_call_static() should return 0; instead of being void.
> And here, instead of barrier_var() tricks,
> it can pass the return from bpf_tail_call_static() further out.

The following should work:

diff --git a/tools/lib/bpf/bpf_helpers.h b/tools/lib/bpf/bpf_helpers.h
index 9d160b5b9c0e..7bf2d6d63436 100644
--- a/tools/lib/bpf/bpf_helpers.h
+++ b/tools/lib/bpf/bpf_helpers.h
@@ -147,7 +147,7 @@
   */
  #if (defined(__clang__) && __clang_major__ >= 8) || (!defined(__clang__) && __GNUC__ > 12)
  #if defined(__bpf__)
-static __always_inline void
+static __always_inline int
  bpf_tail_call_static(void *ctx, const void *map, const __u32 slot)
  {
         if (!__builtin_constant_p(slot))
@@ -172,6 +172,8 @@ bpf_tail_call_static(void *ctx, const void *map, const __u32 slot)
                      "call 12"
                      :: [ctx]"r"(ctx), [map]"r"(map), [slot]"i"(slot)
                      : "r0", "r1", "r2", "r3", "r4", "r5");
+
+       return 0;
  }
  #endif
  #endif

diff --git a/tools/testing/selftests/bpf/progs/verifier_sock.c b/tools/testing/selftests/bpf/progs/verifier_sock.c
index 9f680cf44512..d41a832da928 100644
--- a/tools/testing/selftests/bpf/progs/verifier_sock.c
+++ b/tools/testing/selftests/bpf/progs/verifier_sock.c
@@ -1120,8 +1120,11 @@ int tail_call(struct __sk_buff *sk)
  static __noinline
  int static_tail_call(struct __sk_buff *sk)
  {
-       bpf_tail_call_static(sk, &jmp_table, 0);
-       return 0;
+       int ret;
+
+       ret = bpf_tail_call_static(sk, &jmp_table, 0);
+       barrier_var(ret);
+       return ret;
  }
  
  /* Tail calls in sub-programs invalidate packet pointers. */
@@ -1144,10 +1147,12 @@ __failure __msg("invalid mem access")
  int invalidate_pkt_pointers_by_static_tail_call(struct __sk_buff *sk)
  {
         int *p = (void *)(long)sk->data;
+       int ret;
  
         if ((void *)(p + 1) > (void *)(long)sk->data_end)
                 return TCX_DROP;
-       static_tail_call(sk);
+       ret = static_tail_call(sk);
+       __sink(ret);
         *p = 42; /* this is unsafe */
         return TCX_PASS;
  }

Note that for above static_tail_call(), we cannot do

int static_tail_call(struct __sk_buff *sk)
{
	return bpf_tail_call_static(sk, &jmp_table, 0);
}

The following is some llvm optimization dump for
return bpf_tail_call_static(sk, &jmp_table, 0):

; Function Attrs: noinline nounwind
define internal fastcc void @static_tail_call(ptr noundef %0) unnamed_addr #2 !dbg !34454 {
     #dbg_value(ptr %0, !34457, !DIExpression(), !34458)
     #dbg_value(ptr %0, !34388, !DIExpression(), !34459)
     #dbg_value(ptr @jmp_table, !34395, !DIExpression(), !34459)
     #dbg_value(i32 0, !34396, !DIExpression(), !34459)
   tail call void asm sideeffect "r1 = $0\0A\09r2 = $1\0A\09r3 = $2\0A\09call 12", "r,r,i,~{r0},~{r1},~{r2},~{r3},~{r4},~{r5}"(ptr %0, ptr nonnull @jmp_table, i32 0) #7, !dbg !34461, !srcloc !34400
   ret void, !dbg !34462
}

define dso_local range(i32 0, 3) i32 @invalidate_pkt_pointers_by_static_tail_call(ptr noundef %0) #3 section "tc" !dbg !34424 {
...
15:                                               ; preds = %1
   tail call fastcc void @static_tail_call(ptr noundef %0), !dbg !34445
   store i32 0, ptr %2, align 4, !dbg !34446, !DIAssignID !34447
     #dbg_assign(i32 0, !34428, !DIExpression(), !34447, ptr %2, !DIExpression(), !34433)
   call void asm sideeffect "", "=*imr,0"(ptr nonnull elementtype(i32) %2, i32 0) #7, !dbg !34448, !srcloc !34449
   store i32 42, ptr %7, align 4, !dbg !34450
   br label %16, !dbg !34451
...
}

For static_tail_call(), compiler knows the return value 0. So it changed
the signature to 'void static_tail_call(...)'. The value '0' is used in
invalidate_pkt_pointers_by_static_tail_call().

Could you check whether the above change make sense?

BTW, for the helper, it would be great if we can return 'r0'.

static __always_inline int
bpf_tail_call_static(void *ctx, const void *map, const __u32 slot)
{
         if (!__builtin_constant_p(slot))
                 __bpf_unreachable();

         /*
          * Provide a hard guarantee that LLVM won't optimize setting r2 (map
          * pointer) and r3 (constant map index) from _different paths_ ending
          * up at the _same_ call insn as otherwise we won't be able to use the
          * jmpq/nopl retpoline-free patching by the x86-64 JIT in the kernel
          * given they mismatch. See also d2e4c1e6c294 ("bpf: Constant map key
          * tracking for prog array pokes") for details on verifier tracking.
          *
          * Note on clobber list: we need to stay in-line with BPF calling
          * convention, so even if we don't end up using r0, r4, r5, we need
          * to mark them as clobber so that LLVM doesn't end up using them
          * before / after the call.
          */
         asm volatile("r1 = %[ctx]\n\t"
                      "r2 = %[map]\n\t"
                      "r3 = %[slot]\n\t"
                      "call 12"
                      :: [ctx]"r"(ctx), [map]"r"(map), [slot]"i"(slot)
                      : "r0", "r1", "r2", "r3", "r4", "r5");

         return 'r0';
}

But we have

const struct bpf_func_proto bpf_tail_call_proto = {         /* func is 
unused for tail_call, we set it to pass the          * get_helper_proto 
check          */         .func           = BPF_PTR_POISON,         
.gpl_only       = false,         .ret_type       = RET_VOID,         
.arg1_type      = ARG_PTR_TO_CTX,         .arg2_type      = 
ARG_CONST_MAP_PTR,         .arg3_type      = ARG_ANYTHING, }; and for 
RET_VOID, we have below in verifier.c:         switch 
(base_type(ret_type)) {         case RET_INTEGER:                 /* 
sets type to SCALAR_VALUE */                 mark_reg_unknown(env, regs, 
BPF_REG_0);                 break;         case RET_VOID:               
   regs[BPF_REG_0].type = NOT_INIT;                 break; so we cannot 
return 'r0'.

>
>>   }
>>   
>>   SEC("tc")
>>   int entry(struct __sk_buff *skb)
>>   {
>> -	int ret = 1;
>> +	int ret = 1, ret1, ret2, key1 = 0, key2 = 1;
>>   
>>   	clobber_regs_stack();
>>   
>>   	count++;
>> -	subprog_tail(skb);
>> -	subprog_tail(skb);
>> +	ret1 = subprog_tail(skb);
>> +	ret2 = subprog_tail(skb);
>> +
>> +	bpf_map_update_elem(&array, &key1, &ret1, 0);
>> +	bpf_map_update_elem(&array, &key2, &ret2, 0);

__sink(ret1)/__sink(ret2) should work here.

> I'm sure there is a way to do this without introducing maps or global volatile variables.
>
> pw-bot: cr


  reply	other threads:[~2026-06-09 21:30 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-09 16:39 [PATCH bpf-next 0/3] selftests/bpf: Fix tests for llvm23 true signature Yonghong Song
2026-06-09 16:39 ` [PATCH bpf-next 1/3] selftests/bpf: Keep int return type for tailcall subprogs with maps Yonghong Song
2026-06-09 16:48   ` sashiko-bot
2026-06-09 17:02     ` Yonghong Song
2026-06-09 18:00   ` Alexei Starovoitov
2026-06-09 21:30     ` Yonghong Song [this message]
2026-06-09 21:49       ` Alexei Starovoitov
2026-06-09 22:21         ` Yonghong Song
2026-06-09 16:39 ` [PATCH bpf-next 2/3] selftests/bpf: Keep int return type for tailcall subprogs Yonghong Song
2026-06-09 18:42   ` sashiko-bot
2026-06-09 16:40 ` [PATCH bpf-next 3/3] selftests/bpf: Adjust fexit_bpf2bpf ctx layout for llvm23 true signature Yonghong Song
2026-06-09 18:43   ` sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=145c7132-ec43-4238-801d-55d518fb0da8@linux.dev \
    --to=yonghong.song@linux.dev \
    --cc=alexei.starovoitov@gmail.com \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=kernel-team@fb.com \
    --cc=martin.lau@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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.