All of lore.kernel.org
 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 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.