BPF List
 help / color / mirror / Atom feed
From: Ihor Solodrai <ihor.solodrai@linux.dev>
To: Alexei Starovoitov <ast@kernel.org>,
	Andrii Nakryiko <andrii@kernel.org>,
	Daniel Borkmann <daniel@iogearbox.net>,
	Eduard Zingerman <eddyz87@gmail.com>,
	Kumar Kartikeya Dwivedi <memxor@gmail.com>
Cc: bpf@vger.kernel.org
Subject: [PATCH bpf-next v1 3/4] selftests/bpf: Test btf_dump rendering of decl_tags
Date: Wed, 16 Sep 2026 18:20:36 -0700	[thread overview]
Message-ID: <20260917012037.1396254-4-ihor.solodrai@linux.dev> (raw)
In-Reply-To: <20260917012037.1396254-1-ihor.solodrai@linux.dev>

Cover the decl tag rendering the previous patch added. One subtest
builds a BTF with every shape that renders, plus two that must not:

 - a named record with both forms of tag
 - a typedef'd anonymous record, whose attribute precedes the declarator
 - a union
 - a record that also carries the derived packed attribute
 - an anonymous record inlined at a member
 - two records whose tags are added in reverse target order, which is
   what pins the sort: without it the earlier record loses its attribute
 - attributes on a var and on a func, which btf_dump never declares

A second subtest appends types and attributes to a dumper that has
already run. Its last phase adds an attribute for a record emitted two
dumps earlier and checks nothing is rendered for it: an attribute is
part of the type, and btf_dump emits each definition once.

A third covers members. A member attribute and a record one share a key
in the index, so it checks they do not leak into each other's position,
over a function pointer, an array and a bit-field - declarators the
attribute has to follow rather than precede.

A fourth covers typedefs, including a typedef of an anonymous record
carrying three groups at once, binding to the member, the record and
the typedef.

Switch test_ctx__dump_and_compare() to compare_text_to_expected()
while we are here.

Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
 .../selftests/bpf/prog_tests/btf_dump.c       | 343 +++++++++++++++++-
 1 file changed, 342 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/btf_dump.c b/tools/testing/selftests/bpf/prog_tests/btf_dump.c
index fe04a955d46c..6c031e83dad3 100644
--- a/tools/testing/selftests/bpf/prog_tests/btf_dump.c
+++ b/tools/testing/selftests/bpf/prog_tests/btf_dump.c
@@ -224,7 +224,12 @@ static void test_ctx__dump_and_compare(struct test_ctx *t,
 	fflush(t->dump_buf_file);
 	t->dump_buf[t->dump_buf_sz] = 0; /* some libc implementations don't do this */
 
-	ASSERT_STREQ(t->dump_buf, expected_output, message);
+	/*
+	 * The mismatch has already been reported, so this only has to
+	 * register the failure. ASSERT_OK() would append a stale errno to it.
+	 */
+	err = compare_text_to_expected(t->dump_buf, expected_output);
+	ASSERT_EQ(err, 0, message);
 }
 
 static void test_btf_dump_incremental(void)
@@ -328,6 +333,330 @@ static void test_btf_dump_incremental(void)
 	test_ctx__free(&t);
 }
 
+static void test_btf_dump_decl_tags(void)
+{
+	struct test_ctx t = {};
+	struct btf *btf;
+	int id;
+
+	if (test_ctx__init(&t))
+		return;
+
+	btf = t.btf;
+
+	/*
+	 * Generate BTF corresponding to the following C code:
+	 *
+	 * struct s1 { int f; } __attribute__((attr1))
+	 *                      __attribute__((btf_decl_tag("plain_tag")));
+	 * typedef struct { int f; } __attribute__((a1)) __attribute__((a2)) t1;
+	 * union u1 { int f; } __attribute__((uattr));
+	 * struct p1 { char c; int i; } __attribute__((packed)) __attribute__((pattr));
+	 * struct outer { struct { int g; } __attribute__((inner)) in; };
+	 * struct s2 { int f; } __attribute__((on_s2));
+	 * struct s3 { int f; } __attribute__((on_s3));
+	 *
+	 * plus attributes on a var and a func, which btf_dump does not declare.
+	 */
+	id = btf__add_int(btf, "int", 4, BTF_INT_SIGNED);
+	ASSERT_EQ(id, 1, "int_id");
+	id = btf__add_int(btf, "char", 1, BTF_INT_CHAR);
+	ASSERT_EQ(id, 2, "char_id");
+
+	/* a named record with one attribute of each form */
+	id = btf__add_struct(btf, "s1", 4);
+	ASSERT_EQ(id, 3, "s1_id");
+	ASSERT_OK(btf__add_field(btf, "f", 1, 0, 0), "s1_field");
+	ASSERT_EQ(btf__add_decl_attr(btf, "attr1", 3, -1), 4, "s1_attr");
+	/* no kflag: rendered as a btf_decl_tag(), not as a bare attribute */
+	ASSERT_EQ(btf__add_decl_tag(btf, "plain_tag", 3, -1), 5, "s1_plain_tag");
+
+	/*
+	 * an anonymous record behind a typedef, with two attributes. This is
+	 * the case that cannot be expressed by a caller appending to the dump:
+	 * the attribute has to go before the declarator.
+	 */
+	id = btf__add_struct(btf, NULL, 4);
+	ASSERT_EQ(id, 6, "anon_id");
+	ASSERT_OK(btf__add_field(btf, "f", 1, 0, 0), "anon_field");
+	ASSERT_EQ(btf__add_decl_attr(btf, "a1", 6, -1), 7, "anon_attr1");
+	ASSERT_EQ(btf__add_decl_attr(btf, "a2", 6, -1), 8, "anon_attr2");
+	id = btf__add_typedef(btf, "t1", 6);
+	ASSERT_EQ(id, 9, "typedef_id");
+
+	/* unions are records too */
+	id = btf__add_union(btf, "u1", 4);
+	ASSERT_EQ(id, 10, "u1_id");
+	ASSERT_OK(btf__add_field(btf, "f", 1, 0, 0), "u1_field");
+	ASSERT_EQ(btf__add_decl_attr(btf, "uattr", 10, -1), 11, "u1_attr");
+
+	/* composes with the packed attribute, which is derived from layout */
+	id = btf__add_struct(btf, "p1", 5);
+	ASSERT_EQ(id, 12, "p1_id");
+	ASSERT_OK(btf__add_field(btf, "c", 2, 0, 0), "p1_field_c");
+	ASSERT_OK(btf__add_field(btf, "i", 1, 8, 0), "p1_field_i");
+	ASSERT_EQ(btf__add_decl_attr(btf, "pattr", 12, -1), 13, "p1_attr");
+
+	/* an anonymous record inlined at a member, the third emission position */
+	id = btf__add_struct(btf, NULL, 4);
+	ASSERT_EQ(id, 14, "inner_id");
+	ASSERT_OK(btf__add_field(btf, "g", 1, 0, 0), "inner_field");
+	ASSERT_EQ(btf__add_decl_attr(btf, "inner", 14, -1), 15, "inner_attr");
+	id = btf__add_struct(btf, "outer", 4);
+	ASSERT_EQ(id, 16, "outer_id");
+	ASSERT_OK(btf__add_field(btf, "in", 14, 0, 0), "outer_field");
+
+	/* not rendered: a var has no declaration in the C output */
+	id = btf__add_var(btf, "v", BTF_VAR_GLOBAL_ALLOCATED, 3);
+	ASSERT_EQ(id, 17, "var_id");
+	ASSERT_EQ(btf__add_decl_attr(btf, "var_attr", 17, -1), 18, "var_attr");
+	/* not rendered: neither does a func */
+	id = btf__add_func_proto(btf, 1);
+	ASSERT_EQ(id, 19, "proto_id");
+	id = btf__add_func(btf, "fn", BTF_FUNC_GLOBAL, 19);
+	ASSERT_EQ(id, 20, "func_id");
+	ASSERT_EQ(btf__add_decl_attr(btf, "func_attr", 20, -1), 21, "func_attr");
+
+	/*
+	 * Tags added in reverse target order: the tag with the lower type ID
+	 * names the record with the higher one, so the index is in emission
+	 * order only once btf_dump_resize() has sorted it. Without the sort
+	 * the lookup for s2 lands outside its run and drops the attribute.
+	 */
+	id = btf__add_struct(btf, "s2", 4);
+	ASSERT_EQ(id, 22, "s2_id");
+	ASSERT_OK(btf__add_field(btf, "f", 1, 0, 0), "s2_field");
+	id = btf__add_struct(btf, "s3", 4);
+	ASSERT_EQ(id, 23, "s3_id");
+	ASSERT_OK(btf__add_field(btf, "f", 1, 0, 0), "s3_field");
+	ASSERT_EQ(btf__add_decl_attr(btf, "on_s3", 23, -1), 24, "s3_attr");
+	ASSERT_EQ(btf__add_decl_attr(btf, "on_s2", 22, -1), 25, "s2_attr");
+
+	test_ctx__dump_and_compare(&t,
+"struct s1 {\n"
+"	int f;\n"
+"} __attribute__((attr1)) __attribute__((btf_decl_tag(\"plain_tag\")));\n"
+"\n"
+"typedef struct {\n"
+"	int f;\n"
+"} __attribute__((a1)) __attribute__((a2)) t1;\n"
+"\n"
+"union u1 {\n"
+"	int f;\n"
+"} __attribute__((uattr));\n"
+"\n"
+"struct p1 {\n"
+"	char c;\n"
+"	int i;\n"
+"} __attribute__((packed)) __attribute__((pattr));\n"
+"\n"
+"struct outer {\n"
+"	struct {\n"
+"		int g;\n"
+"	} __attribute__((inner)) in;\n"
+"};\n"
+"\n"
+"struct s2 {\n"
+"	int f;\n"
+"} __attribute__((on_s2));\n"
+"\n"
+"struct s3 {\n"
+"	int f;\n"
+"} __attribute__((on_s3));\n"
+"\n", "dump_and_compare");
+
+	test_ctx__free(&t);
+}
+
+/*
+ * btf_dump indexes decl attributes incrementally, only walking types appended
+ * since the last dump. Check that attributes on types added to an existing
+ * dumper are picked up, and that one added for a record that has already been
+ * emitted is not - btf_dump emits each definition once.
+ */
+static void test_btf_dump_decl_tags_incremental(void)
+{
+	struct test_ctx t = {};
+	struct btf *btf;
+	int id;
+
+	if (test_ctx__init(&t))
+		return;
+
+	btf = t.btf;
+
+	id = btf__add_int(btf, "int", 4, BTF_INT_SIGNED);
+	ASSERT_EQ(id, 1, "int_id");
+	id = btf__add_struct(btf, "s1", 4);
+	ASSERT_EQ(id, 2, "s1_id");
+	ASSERT_OK(btf__add_field(btf, "f", 1, 0, 0), "s1_field");
+	ASSERT_EQ(btf__add_decl_attr(btf, "a1", 2, -1), 3, "s1_attr");
+
+	test_ctx__dump_and_compare(&t,
+"struct s1 {\n"
+"	int f;\n"
+"} __attribute__((a1));\n"
+"\n", "first_dump");
+
+	fseek(t.dump_buf_file, 0, SEEK_SET);
+
+	/* a new record and a new attribute for it */
+	id = btf__add_struct(btf, "s2", 4);
+	ASSERT_EQ(id, 4, "s2_id");
+	ASSERT_OK(btf__add_field(btf, "f", 1, 0, 0), "s2_field");
+	ASSERT_EQ(btf__add_decl_attr(btf, "a2", 4, -1), 5, "s2_attr");
+
+	test_ctx__dump_and_compare(&t,
+"struct s2 {\n"
+"	int f;\n"
+"} __attribute__((a2));\n"
+"\n", "second_dump");
+
+	fseek(t.dump_buf_file, 0, SEEK_SET);
+
+	/* s1 has already been emitted, so "late" is not rendered anywhere */
+	ASSERT_EQ(btf__add_decl_attr(btf, "late", 2, -1), 6, "s1_late_attr");
+	id = btf__add_struct(btf, "s3", 4);
+	ASSERT_EQ(id, 7, "s3_id");
+	ASSERT_OK(btf__add_field(btf, "f", 1, 0, 0), "s3_field");
+	ASSERT_EQ(btf__add_decl_attr(btf, "a3", 7, -1), 8, "s3_attr");
+
+	test_ctx__dump_and_compare(&t,
+"struct s3 {\n"
+"	int f;\n"
+"} __attribute__((a3));\n"
+"\n", "third_dump");
+
+	test_ctx__free(&t);
+}
+
+/*
+ * An attribute on a member is emitted at the member declaration, and one on
+ * the record at the closing brace. Both are keyed by the record in the index,
+ * so this also checks that they do not leak into each other's position.
+ */
+static void test_btf_dump_decl_tags_members(void)
+{
+	struct test_ctx t = {};
+	struct btf *btf;
+	int id;
+
+	if (test_ctx__init(&t))
+		return;
+
+	btf = t.btf;
+
+	/*
+	 * Generate BTF corresponding to the following C code:
+	 *
+	 * struct m1 {
+	 *	int (*fp)(void) __attribute__((fp_attr));
+	 *	int a;
+	 *	int b __attribute__((b1)) __attribute__((b2));
+	 *	int arr[4] __attribute__((arr_attr));
+	 * } __attribute__((rec));
+	 * struct bits { int x : 3 __attribute__((x_attr)); int y : 29; };
+	 */
+	id = btf__add_int(btf, "int", 4, BTF_INT_SIGNED);
+	ASSERT_EQ(id, 1, "int_id");
+	id = btf__add_array(btf, 1, 1, 4);
+	ASSERT_EQ(id, 2, "array_id");
+	id = btf__add_func_proto(btf, 1);
+	ASSERT_EQ(id, 3, "proto_id");
+	id = btf__add_ptr(btf, 3);
+	ASSERT_EQ(id, 4, "ptr_id");
+
+	id = btf__add_struct(btf, "m1", 32);
+	ASSERT_EQ(id, 5, "m1_id");
+	ASSERT_OK(btf__add_field(btf, "fp", 4, 0, 0), "m1_fp");
+	ASSERT_OK(btf__add_field(btf, "a", 1, 64, 0), "m1_a");
+	ASSERT_OK(btf__add_field(btf, "b", 1, 96, 0), "m1_b");
+	ASSERT_OK(btf__add_field(btf, "arr", 2, 128, 0), "m1_arr");
+	ASSERT_EQ(btf__add_decl_attr(btf, "rec", 5, -1), 6, "m1_rec_attr");
+	/* the attribute follows the whole declarator, pointer and array alike */
+	ASSERT_EQ(btf__add_decl_attr(btf, "fp_attr", 5, 0), 7, "m1_fp_attr");
+	/* two on one member, to pin the ordering */
+	ASSERT_EQ(btf__add_decl_attr(btf, "b1", 5, 2), 8, "m1_b1");
+	ASSERT_EQ(btf__add_decl_attr(btf, "b2", 5, 2), 9, "m1_b2");
+	ASSERT_EQ(btf__add_decl_attr(btf, "arr_attr", 5, 3), 10, "m1_arr_attr");
+
+	/* a bit-field: the attribute has to follow the width */
+	id = btf__add_struct(btf, "bits", 4);
+	ASSERT_EQ(id, 11, "bits_id");
+	ASSERT_OK(btf__add_field(btf, "x", 1, 0, 3), "bits_x");
+	ASSERT_OK(btf__add_field(btf, "y", 1, 3, 29), "bits_y");
+	ASSERT_EQ(btf__add_decl_attr(btf, "x_attr", 11, 0), 12, "bits_x_attr");
+
+	test_ctx__dump_and_compare(&t,
+"struct m1 {\n"
+"	int (*fp)(void) __attribute__((fp_attr));\n"
+"	int a;\n"
+"	int b __attribute__((b1)) __attribute__((b2));\n"
+"	int arr[4] __attribute__((arr_attr));\n"
+"} __attribute__((rec));\n"
+"\n"
+"struct bits {\n"
+"	int x: 3 __attribute__((x_attr));\n"
+"	int y: 29;\n"
+"};\n"
+"\n", "dump_and_compare");
+
+	test_ctx__free(&t);
+}
+
+/*
+ * A typedef is the third and last kind btf_dump declares. Its attributes go
+ * after the declarator, so a typedef of an anonymous record can carry three
+ * groups in one declaration, binding to three different entities.
+ */
+static void test_btf_dump_decl_tags_typedef(void)
+{
+	struct test_ctx t = {};
+	struct btf *btf;
+	int id;
+
+	if (test_ctx__init(&t))
+		return;
+
+	btf = t.btf;
+
+	/*
+	 * Generate BTF corresponding to the following C code:
+	 *
+	 * typedef int td __attribute__((t1))
+	 *                __attribute__((btf_decl_tag("t2")));
+	 * typedef struct {
+	 *	int f __attribute__((memb_attr));
+	 * } __attribute__((rec_attr)) both __attribute__((both_attr));
+	 */
+	id = btf__add_int(btf, "int", 4, BTF_INT_SIGNED);
+	ASSERT_EQ(id, 1, "int_id");
+	id = btf__add_typedef(btf, "td", 1);
+	ASSERT_EQ(id, 2, "td_id");
+	ASSERT_EQ(btf__add_decl_attr(btf, "t1", 2, -1), 3, "td_attr1");
+	/* no kflag: rendered as a btf_decl_tag(), not as a bare attribute */
+	ASSERT_EQ(btf__add_decl_tag(btf, "t2", 2, -1), 4, "td_attr2");
+
+	id = btf__add_struct(btf, NULL, 4);
+	ASSERT_EQ(id, 5, "anon_id");
+	ASSERT_OK(btf__add_field(btf, "f", 1, 0, 0), "anon_field");
+	ASSERT_EQ(btf__add_decl_attr(btf, "rec_attr", 5, -1), 6, "anon_rec_attr");
+	ASSERT_EQ(btf__add_decl_attr(btf, "memb_attr", 5, 0), 7, "anon_memb_attr");
+	id = btf__add_typedef(btf, "both", 5);
+	ASSERT_EQ(id, 8, "both_id");
+	ASSERT_EQ(btf__add_decl_attr(btf, "both_attr", 8, -1), 9, "both_attr");
+
+	test_ctx__dump_and_compare(&t,
+"typedef int td __attribute__((t1)) __attribute__((btf_decl_tag(\"t2\")));\n"
+"\n"
+"typedef struct {\n"
+"	int f __attribute__((memb_attr));\n"
+"} __attribute__((rec_attr)) both __attribute__((both_attr));\n"
+"\n", "dump_and_compare");
+
+	test_ctx__free(&t);
+}
+
 static void test_btf_dump_type_tags(void)
 {
 	struct test_ctx t = {};
@@ -1109,6 +1438,18 @@ void test_btf_dump() {
 	if (test__start_subtest("btf_dump: incremental"))
 		test_btf_dump_incremental();
 
+	if (test__start_subtest("btf_dump: decl_tags"))
+		test_btf_dump_decl_tags();
+
+	if (test__start_subtest("btf_dump: decl_tags_incremental"))
+		test_btf_dump_decl_tags_incremental();
+
+	if (test__start_subtest("btf_dump: decl_tags_members"))
+		test_btf_dump_decl_tags_members();
+
+	if (test__start_subtest("btf_dump: decl_tags_typedef"))
+		test_btf_dump_decl_tags_typedef();
+
 	if (test__start_subtest("btf_dump: type_tags"))
 		test_btf_dump_type_tags();
 
-- 
2.55.0


  parent reply	other threads:[~2026-09-17  1:21 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-17  1:20 [PATCH bpf-next v1 0/4] libbpf: Render decl_tags in btf_dump Ihor Solodrai
2026-09-17  1:20 ` [PATCH bpf-next v1 1/4] libbpf: Walk types in btf_dump_resize(), not in mark_referenced() Ihor Solodrai
2026-09-17 22:32   ` Eduard Zingerman
2026-09-17  1:20 ` [PATCH bpf-next v1 2/4] libbpf: Render decl_tags in btf_dump Ihor Solodrai
2026-09-17  1:30   ` sashiko-bot
2026-09-17  2:12   ` Alexei Starovoitov
2026-09-17  3:25     ` Ihor Solodrai
2026-09-17 22:46   ` Eduard Zingerman
2026-09-17 23:59   ` Andrii Nakryiko
2026-09-22 16:59     ` Ihor Solodrai
2026-09-22 19:43       ` Andrii Nakryiko
2026-09-17  1:20 ` Ihor Solodrai [this message]
2026-09-17 10:30   ` [PATCH bpf-next v1 3/4] selftests/bpf: Test btf_dump rendering of decl_tags Alan Maguire
2026-09-17 17:38     ` Ihor Solodrai
2026-09-17 23:18   ` Eduard Zingerman
2026-09-17  1:20 ` [PATCH bpf-next v1 4/4] selftests/bpf: Show the tag kflag in the raw BTF dump helper Ihor Solodrai
2026-09-17  1:27   ` sashiko-bot
2026-09-17 23:21   ` Eduard Zingerman

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=20260917012037.1396254-4-ihor.solodrai@linux.dev \
    --to=ihor.solodrai@linux.dev \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=eddyz87@gmail.com \
    --cc=memxor@gmail.com \
    /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