BPF List
 help / color / mirror / Atom feed
From: Yonghong Song <yonghong.song@linux.dev>
To: bot+bpf-ci@kernel.org, bpf@vger.kernel.org
Cc: ast@kernel.org, andrii@kernel.org, daniel@iogearbox.net,
	eddyz87@gmail.com, kernel-team@fb.com, martin.lau@kernel.org,
	clm@meta.com, ihor.solodrai@linux.dev
Subject: Re: [PATCH bpf-next v4 11/13] selftests/bpf: Add inline-asm and subprog tests for R0:R2 returns
Date: Wed, 12 Aug 2026 14:08:54 -0700	[thread overview]
Message-ID: <7284d446-b51e-407a-ba9c-fd42c8a8a99e@linux.dev> (raw)
In-Reply-To: <47093952550af2c87bc5730d0d9cec8f1c6e622cfb0216a9b1256118d8413274@mail.kernel.org>



On 8/10/26 6:24 PM, bot+bpf-ci@kernel.org wrote:
>> diff --git a/tools/testing/selftests/bpf/prog_tests/aggregate_ret.c b/tools/testing/selftests/bpf/prog_tests/aggregate_ret.c
>> --- a/tools/testing/selftests/bpf/prog_tests/aggregate_ret.c
>> +++ b/tools/testing/selftests/bpf/prog_tests/aggregate_ret.c
>> @@ -5,6 +5,9 @@
>>   #include "aggregate_ret_struct_c.skel.h"
>>   #include "aggregate_ret_union_c.skel.h"
>>   #include "aggregate_ret_kfunc_c.skel.h"
>> +#include "aggregate_ret_run.skel.h"
>> +#include "aggregate_ret_func.skel.h"
>> +#include "aggregate_ret_kfunc.skel.h"
> [ ... ]
>
>> @@ -139,10 +142,65 @@ static void test_kfunc_c(void)
>>   	aggregate_ret_kfunc_c__destroy(skel);
>>   }
>>
>> +static void test_run(void)
>> +{
>> +	struct aggregate_ret_run *skel;
>> +	bool kfunc_ok = true;
>> +	int err;
>> +
>> +	/*
>> +	 * Every program in this object shares __kfunc_btf_root(), so where the
>> +	 * testmod kfuncs are absent the object cannot load at all -- including
>> +	 * for the kfunc-free "asm" subtest.
>> +	 */
>> +	if (!has_ret_pair_kfuncs()) {
>> +		run_subtest("asm", NULL, false);
>> +		run_subtest("asm_kfunc", NULL, false);
>> +		run_subtest("struct", NULL, false);
>> +		run_subtest("union", NULL, false);
>> +		return;
>> +	}
>> +
>> +	skel = aggregate_ret_run__open();
>> +	if (!ASSERT_OK_PTR(skel, "skel_run_open"))
>> +		return;
>> +
>> +	err = aggregate_ret_run__load(skel);
>> +	if (err == -EOPNOTSUPP) {
>> +		kfunc_ok = false;
>> +		aggregate_ret_run__destroy(skel);
>> +
>> +		skel = aggregate_ret_run__open();
>> +		if (!ASSERT_OK_PTR(skel, "skel_run_reopen"))
>> +			return;
>> +
>> +		bpf_program__set_autoload(skel->progs.aggregate_ret_asm_kfunc_test, false);
>> +		bpf_program__set_autoload(skel->progs.aggregate_ret_struct_test, false);
>> +		bpf_program__set_autoload(skel->progs.aggregate_ret_union_test, false);
>> +
>> +		err = aggregate_ret_run__load(skel);
>> +	}
>> +	if (!ASSERT_OK(err, "skel_run_load"))
>> +		goto out;
> A subsystem pattern flags this as potentially concerning:
> has_ret_pair_kfuncs() checks only for x86_64 or arm64 architecture at
> compile time. On those architectures, if bpf_testmod.ko is not actually
> loaded at runtime, aggregate_ret_run__load() returns -ESRCH rather than
> -EOPNOTSUPP, causing the reopen branch to be skipped and the test to
> report a hard FAIL rather than skipping. The comment states "where the
> testmod kfuncs are absent the object cannot load at all" but the code
> only guards one form of "absent" (architecture) and not the other
> (module not loaded).
>
> test_progs tracks whether bpf_testmod.ko is loaded in env.has_testmod
> and sets it to false if load_bpf_testmod() fails. Every other
> testmod-dependent test in the tree checks both the architecture
> capability and env.has_testmod before attempting to use testmod kfuncs.
> Examples include bpf_cookie.c, iters.c, ksyms_module.c, btf_module.c,
> btf_tag.c, tp_btf_nullable.c, module_fentry_shadow.c, and
> core_reloc.c.
>
> Should test_run() fold env.has_testmod into its guard to match the
> established pattern, so that the test skips cleanly in all environments
> where bpf_testmod is unavailable?

Yes, we should add env.has_testmod.

>
>> +
>> +	run_subtest("asm", skel->progs.aggregate_ret_asm_test, true);
>> +	run_subtest("asm_kfunc", skel->progs.aggregate_ret_asm_kfunc_test, kfunc_ok);
>> +	run_subtest("struct", skel->progs.aggregate_ret_struct_test, kfunc_ok);
>> +	run_subtest("union", skel->progs.aggregate_ret_union_test, kfunc_ok);
>> +
>> +out:
>> +	aggregate_ret_run__destroy(skel);
>> +}
>> +
>>   void test_aggregate_ret(void)
>>   {
>>   	test_int128_c();
>>   	test_struct_c();
>>   	test_union_c();
>>   	test_kfunc_c();
>> +	test_run();
>> +
>> +	RUN_TESTS(aggregate_ret_func);
>> +	if (has_ret_pair_kfuncs())
>> +		RUN_TESTS(aggregate_ret_kfunc);
>>   }
> A subsystem pattern flags this as potentially concerning: the four
> test_run() subtests are registered as named skips when kfuncs are
> unavailable, with a comment stating "the list of subtests does not
> depend on the compiler or on the architecture." RUN_TESTS(aggregate_ret_kfunc)
> conditionally disappears, so the five aggregate_ret_kfunc_* subtests do
> not appear at all outside x86_64/arm64 - the subtest list does depend
> on the architecture. Anyone diffing test_progs output across
> architectures sees subtests vanish rather than skip. The two halves of
> the change implement opposite policies about whether an unavailable
> prerequisite should produce a visible skip.
>
> Should RUN_TESTS(aggregate_ret_kfunc) use run_subtest() to produce
> named skips, matching the approach a few lines earlier?

Right, we can do skip. In the future, when additional arch
gets support, those 'skip' can be changed to actual run.

>
>> diff --git a/tools/testing/selftests/bpf/prog_tests/fexit_bpf2bpf.c b/tools/testing/selftests/bpf/prog_tests/fexit_bpf2bpf.c
>> --- a/tools/testing/selftests/bpf/prog_tests/fexit_bpf2bpf.c
>> +++ b/tools/testing/selftests/bpf/prog_tests/fexit_bpf2bpf.c
> [ ... ]
>
>> diff --git a/tools/testing/selftests/bpf/progs/verifier_arena.c b/tools/testing/selftests/bpf/progs/verifier_arena.c
>> --- a/tools/testing/selftests/bpf/progs/verifier_arena.c
>> +++ b/tools/testing/selftests/bpf/progs/verifier_arena.c
>> @@ -704,4 +704,42 @@ int check_arena_arg_ret(void *ctx)
>>   	return 0;
>>   }
>>
>> +struct arena_ret_pair {
>> +	__u64 lo;
>> +	__u64 hi;
>> +};
>> +
>> +/*
>> + * A 16-byte value is returned in the R0:R2 register pair. A global subprogram
>> + * may return an arena pointer in R0, but R2 holds the upper half of a scalar
>> + * pair, so an arena pointer there is not a valid return value. The ld_imm64 of
>> + * the arena map is what links the arena to the program, without which the
>> + * addr_space_cast insn is not allowed.
>> + */
>> +__naked struct arena_ret_pair global_ret_arena_ptr_in_r2(void)
>> +{
>> +	asm volatile (
>> +		"r1 = %[arena] ll;"
>> +		"r2 = 8192;"
>> +		"r2 = addr_space_cast(r2, 0x0, 0x1);"
>> +		"r0 = 0;"
>> +		"exit;"
>> +		:
>> +		: __imm_addr(arena)
>> +		: __clobber_all);
>> +}
> Does this inline-asm instruction build on clang older than LLVM 18?
> The addr_space_cast mnemonic was introduced in LLVM 18 along with
> __BPF_FEATURE_ADDR_SPACE_CAST. Every other inline-asm use of
> addr_space_cast in tools/testing/selftests/bpf/progs/ is guarded:
>
> verifier_arena.c at lines 511-638 wraps six uses inside '#if
> defined(__BPF_FEATURE_ADDR_SPACE_CAST)'.
>
> arena_atomics.c guards its uses with '#if defined(ENABLE_ATOMICS_TESTS)
> && defined(__BPF_FEATURE_ADDR_SPACE_CAST)'.
>
> verifier_zext.c guards with '#ifdef __BPF_FEATURE_ADDR_SPACE_CAST'.
>
> compute_live_registers.c guards with '#ifdef
> __BPF_FEATURE_ADDR_SPACE_CAST'.
>
> verifier_ldsx.c guards with '__clang_major__ >= 18'.
>
> The new code here is the only unguarded occurrence in the tree. On
> older clang this asm string will not parse and verifier_arena.bpf.o
> will fail to build.

The CI is using llvm21, so we should be okay.

>
>> diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
>> --- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
>> +++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
> [ ... ]
>
>> diff --git a/tools/testing/selftests/bpf/progs/aggregate_ret_func.c b/tools/testing/selftests/bpf/progs/aggregate_ret_func.c
>> --- /dev/null
>> +++ b/tools/testing/selftests/bpf/progs/aggregate_ret_func.c
>> @@ -0,0 +1,420 @@
> [ ... ]
>
>> +#if __clang_major__ >= 23
>> +
>> +struct pair {
>> +	__u64 hi;
>> +	__u64 lo;
>> +};
>> +
>> +union upair {
>> +	__u64 halves[2];
>> +	struct {
>> +		__u64 lo;
>> +		__u64 hi;
>> +	} parts;
>> +};
>> +
>> +/* A by-value struct that smuggles a pointer, which must be rejected. */
>> +struct with_ptr {
>> +	void *p;
>> +	__u64 x;
>> +};
>> +
>> +/* A by-value union that smuggles a pointer, which must be rejected too. */
>> +union upair_with_ptr {
>> +	void *p;
>> +	__u64 halves[2];
>> +};
>> +
>> +/* Global subprogram returning a scalar-only 16-byte struct in R0:R2. */
>> +__naked struct pair global_ret_struct(void)
>> +{
>> +	asm volatile (
>> +	"r0 = 0x1234;"	/* struct's first half */
>> +	"r2 = 0x5678;"	/* struct's second half */
>> +	"exit;"
>> +	);
>> +}
>> +
>> +/* Global subprogram returning a scalar-only 16-byte union in R0:R2. */
>> +__naked union upair global_ret_union(void)
>> +{
>> +	asm volatile (
>> +	"r0 = 0x1234;"
>> +	"r2 = 0x5678;"
>> +	"exit;"
>> +	);
>> +}
> [ ... ]
>
>> +__naked struct with_ptr global_ret_struct_ptr(void)
>> +{
>> +	asm volatile (
>> +	"r0 = 0;"
>> +	"r2 = 0;"
>> +	"exit;"
>> +	);
>> +}
>> +
>> +SEC("tc")
>> +__failure __msg("Global function global_ret_struct_ptr() has unsupported return type")
>> +__naked int aggregate_ret_global_struct_ptr_fail(void)
>> +{
>> +	asm volatile (
>> +	"call %[global_ret_struct_ptr];"
>> +	"r0 = 0;"
>> +	"exit;"
>> +	:
>> +	: __imm(global_ret_struct_ptr)
>> +	: __clobber_all);
>> +}
>> +
>> +__naked union upair_with_ptr global_ret_union_ptr(void)
>> +{
>> +	asm volatile (
>> +	"r0 = 0;"
>> +	"r2 = 0;"
>> +	"exit;"
>> +	);
>> +}
>> +
>> +SEC("tc")
>> +__failure __msg("Global function global_ret_union_ptr() has unsupported return type")
>> +__naked int aggregate_ret_global_union_ptr_fail(void)
>> +{
>> +	asm volatile (
>> +	"call %[global_ret_union_ptr];"
>> +	"r0 = 0;"
>> +	"exit;"
>> +	:
>> +	: __imm(global_ret_union_ptr)
>> +	: __clobber_all);
>> +}
>> +
>> +#endif /* __clang_major__ >= 23 */
>> +
>> +static __naked u128 agg_callee(void)
>> +{
>> +	asm volatile (
>> +	"r0 = 1;"
>> +	"r2 = 2;"
>> +	"exit;"
>> +	);
>> +}
> [ ... ]
>
>> +struct ptr_pair {
>> +	void *p;
>> +	__u64 x;
>> +};
>> +
>> +static __naked __noinline struct ptr_pair static_ret_ptr_pair(void)
>> +{
>> +	asm volatile (
>> +	"r0 = 0;"
>> +	"r2 = r1;"
>> +	"exit;"
>> +	);
>> +}
>> +
>> +SEC("tc")
>> +__success __retval(0)
>> +__naked int aggregate_ret_static_ptr_pair(void)
>> +{
>> +	asm volatile (
>> +	"call %[static_ret_ptr_pair];"
>> +	"r1 = *(u32 *)(r2 + 0);"	/* deref the returned ctx pointer */
>> +	"r0 = 0;"
>> +	"exit;"
>> +	:
>> +	: __imm(static_ret_ptr_pair)
>> +	: __clobber_all);
>> +}
> The file treats the same construct - a __naked subprogram whose C
> prototype returns a 16-byte aggregate - two different ways, and exactly
> one of the two must be wrong.
>
> Inside the guard at lines 218-349 sit global_ret_struct (struct pair),
> global_ret_union (union upair), global_ret_struct_ptr (struct
> with_ptr) and global_ret_union_ptr (union upair_with_ptr), plus their
> four test programs. Outside the guard, after #endif, lines 392-418 add
> struct ptr_pair, static_ret_ptr_pair() returning struct ptr_pair, and
> aggregate_ret_static_ptr_pair() - an unguarded naked subprogram
> returning a 16-byte struct by value, in the same file.
>
> If the guard is unnecessary, then the four guarded tests are dead on
> every toolchain in use today. That costs test coverage:
> 'aggregate_ret_global_struct_ptr_fail' and
> 'aggregate_ret_global_union_ptr_fail' are the only coverage anywhere in
> the tree for the __btf_type_is_scalar_struct() rejection added by this
> series to btf_validate_return_type() in kernel/bpf/btf.c. That check is
> the security-relevant half of the new convention - it stops a global
> subprogram from laundering a pointer to its caller through half of an
> "opaque scalar pair". Under this interpretation that check ships with
> zero test coverage until clang 23 is widely deployed, and the two
> positive tests 'aggregate_ret_global_struct' /
> 'aggregate_ret_global_union' (the only ones proving a struct/union - as
> opposed to __int128 - actually reaches R0:R2) are dead too.
>
> If the guard is necessary, then static_ret_ptr_pair is broken on clang
> < 23. The commit message states "a by-value return larger than 16 bytes
> is lowered to an sret pointer argument and the BTF the verifier reads
> says the function returns void". If a pre-23 backend does the same for
> any by-value aggregate return, then on clang < 23 static_ret_ptr_pair's
> BTF says void (or gains a hidden sret argument),
> bpf_ret_reg_pair(env, subprog) is false, prepare_func_exit() copies
> only R0 back into the caller, and the caller's 'r1 = *(u32 *)(r2 + 0)'
> hits R2 == NOT_INIT - the naked entry never wrote it. That would be 'R2
> !read_ok' at verifier.c, so a test tagged '__success __retval(0)' fails
> to load. test_aggregate_ret() calls RUN_TESTS(aggregate_ret_func)
> unconditionally - no arch or compiler gate - so this would be a hard
> FAIL for essentially every current toolchain.
>
> Evidence points at the guard being the mistake: (1) the same commit
> adds '__naked struct arena_ret_pair global_ret_arena_ptr_in_r2(void)'
> to progs/verifier_arena.c unguarded; (2) the same commit adds '__naked
> u128 agg_ret_target_func(void)' in progs/aggregate_ret_target.c
> unguarded; (3) progs/aggregate_ret_run.c's own comment says taking the
> address rather than calling "avoids any dependence on the compiler
> lowering an __int128 or struct return value". A naked body has no
> return statement to lower, so the source-level prototype is what lands
> in BTF.
>
> Either way the asymmetry needs resolving. If the guard is superfluous,
> could it be dropped or replaced with the '#if defined(__clang_major__)
> && __clang_major__ >= 23' + '#else' fallback shape used by
> aggregate_ret_struct_c.c? If it is not superfluous, should
> static_ret_ptr_pair and aggregate_ret_static_ptr_pair move inside it,
> and should progs/verifier_arena.c receive the same treatment?
>
> The lore discussion with Eduard Zingerman on v1 suggested moving the
> liveness test (aggregate_ret_live) to compute_live_registers.c, where
> such tests are usually added. You agreed ('I can move the test to
> compute_live_registers.c') but the test remains in aggregate_ret_func.c
> in v4. Should aggregate_ret_live be moved to
> compute_live_registers.c to follow the established pattern?

Okay, will do.

>
> The same v1 review suggested keeping only the C version of the global
> union test (not assembly) to reduce duplication. The assembly version
> aggregate_ret_global_union still exists in aggregate_ret_func.c. Would
> dropping the assembly version match the intent?

Let me check if indeed there are some redundancy.

>
>> diff --git a/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc.c b/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc.c
>> --- /dev/null
>> +++ b/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc.c
> [ ... ]
>
>> diff --git a/tools/testing/selftests/bpf/progs/aggregate_ret_run.c b/tools/testing/selftests/bpf/progs/aggregate_ret_run.c
>> --- /dev/null
>> +++ b/tools/testing/selftests/bpf/progs/aggregate_ret_run.c
> [ ... ]
>
>> diff --git a/tools/testing/selftests/bpf/progs/aggregate_ret_target.c b/tools/testing/selftests/bpf/progs/aggregate_ret_target.c
>> --- /dev/null
>> +++ b/tools/testing/selftests/bpf/progs/aggregate_ret_target.c
> [ ... ]
>
>> diff --git a/tools/testing/selftests/bpf/progs/freplace_ret_pair.c b/tools/testing/selftests/bpf/progs/freplace_ret_pair.c
>> --- /dev/null
>> +++ b/tools/testing/selftests/bpf/progs/freplace_ret_pair.c
> [ ... ]
>
>
> ---
> 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/31446101762


  reply	other threads:[~2026-08-12 21:09 UTC|newest]

Thread overview: 37+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-11  0:09 [PATCH bpf-next v4 00/13] bpf: Support aggregate return values up to 16 bytes Yonghong Song
2026-08-11  0:09 ` [PATCH bpf-next v4 01/13] bpf: Factor check_global_ret_scalar_reg() out of the global return check Yonghong Song
2026-08-11  0:09 ` [PATCH bpf-next v4 02/13] bpf: Add helpers to describe the R0:R2 return register pair Yonghong Song
2026-08-11  1:24   ` bot+bpf-ci
2026-08-12 19:31     ` Yonghong Song
2026-08-12 20:07   ` Eduard Zingerman
2026-08-11  0:09 ` [PATCH bpf-next v4 03/13] bpf: Wire up JIT support for 16-byte kfunc returns Yonghong Song
2026-08-11  1:24   ` bot+bpf-ci
2026-08-12 19:48     ` Yonghong Song
2026-08-12 20:42   ` Eduard Zingerman
2026-08-11  0:09 ` [PATCH bpf-next v4 04/13] bpf: Track R2 of register-pair returns in precision backtracking Yonghong Song
2026-08-12 21:16   ` Eduard Zingerman
2026-08-11  0:09 ` [PATCH bpf-next v4 05/13] bpf: Account R2 of register-pair returns in live register analysis Yonghong Song
2026-08-11  1:09   ` bot+bpf-ci
2026-08-12 19:55     ` Yonghong Song
2026-08-12 21:21   ` Eduard Zingerman
2026-08-11  0:09 ` [PATCH bpf-next v4 06/13] bpf: Reject callbacks returning more than 8 bytes Yonghong Song
2026-08-12 21:41   ` Eduard Zingerman
2026-08-11  0:09 ` [PATCH bpf-next v4 07/13] bpf: Add verifier support for 16-byte returns in R0:R2 Yonghong Song
2026-08-11  1:24   ` bot+bpf-ci
2026-08-12 20:12     ` Yonghong Song
2026-08-11  0:09 ` [PATCH bpf-next v4 08/13] bpf: Reject register-pair returns when the subprog BTF is unreliable Yonghong Song
2026-08-11  1:24   ` bot+bpf-ci
2026-08-12 20:26     ` Yonghong Song
2026-08-11  0:09 ` [PATCH bpf-next v4 09/13] bpf: Enable aggregate return types up to 16 bytes Yonghong Song
2026-08-11  1:24   ` bot+bpf-ci
2026-08-12 20:29     ` Yonghong Song
2026-08-11  0:10 ` [PATCH bpf-next v4 10/13] selftests/bpf: Add C tests for 16-byte returns in R0:R2 Yonghong Song
2026-08-11  1:24   ` bot+bpf-ci
2026-08-12 20:49     ` Yonghong Song
2026-08-11  0:10 ` [PATCH bpf-next v4 11/13] selftests/bpf: Add inline-asm and subprog tests for R0:R2 returns Yonghong Song
2026-08-11  1:24   ` bot+bpf-ci
2026-08-12 21:08     ` Yonghong Song [this message]
2026-08-11  0:10 ` [PATCH bpf-next v4 12/13] selftests/bpf: Add tests for callbacks returning more than 8 bytes Yonghong Song
2026-08-11  1:24   ` bot+bpf-ci
2026-08-12 21:22     ` Yonghong Song
2026-08-11  0:10 ` [PATCH bpf-next v4 13/13] Documentation/bpf: Document up to 16-byte kfunc return values in R0:R2 Yonghong Song

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=7284d446-b51e-407a-ba9c-fd42c8a8a99e@linux.dev \
    --to=yonghong.song@linux.dev \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bot+bpf-ci@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=clm@meta.com \
    --cc=daniel@iogearbox.net \
    --cc=eddyz87@gmail.com \
    --cc=ihor.solodrai@linux.dev \
    --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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox