* [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump
@ 2026-09-29 23:46 Ihor Solodrai
2026-09-29 23:46 ` [PATCH bpf-next v2 1/5] libbpf: Walk types in btf_dump_resize(), not in mark_referenced() Ihor Solodrai
` (5 more replies)
0 siblings, 6 replies; 7+ messages in thread
From: Ihor Solodrai @ 2026-09-29 23:46 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: bpf, Alan Maguire
BTF supports arbitrary __attribute__ encoding via decl tags and type
tags [1]. However btf_dump facility only outputs the attributes
encoded in BTF as type tags, which was implemented in commit
2019c58318b8 ("libbpf: Check the kflag of type tags in btf_dump").
BTF_KIND_DECL_TAG has been left out of btf_dump entirely, independent
of the kflag value. An attribute a BTF producer encoded is silently
dropped from the C output.
Implement decl_tag support in btf_dump. The btf_dump limits rendered
BTF types to records, record members and typedefs. Other types,
particularly functions, are deliberately skipped. Therefore decl_tag
rendering works only for the same subset of target types.
To be efficient, decl_tag dump support requires a reverse index from
the target type id to the list of tags pointing at it.
With decl_tag dump support, provided a validly constructed BTF, it is
now possible to obtain C-syntax output like the following example:
struct foo {
int a __attribute__((bar));
int b: 3 __attribute__((bar));
} __attribute__((bar));
typedef struct { ... } __attribute__((bar)) foo_t __attribute__((baz));
This closes a generic gap in btf_dump, which silently dropped every
decl tag it was given. The in-tree consumer is:
bpftool btf dump ... format c
A BPF object now keeps its member tags, such as the "contains:" tags.
[1] https://lore.kernel.org/bpf/20250130201239.1429648-1-ihor.solodrai@linux.dev/
---
v1->v2:
* index all decl tags in btf_dump (Andrii)
* roll the decl tag count back on resize errors (Alexei, sashiko)
* new patch #3 with the ASSERT_TEXT_EQ() text-comparison helper (Eduard)
* fold the incremental subtest into test_btf_dump_incremental (Eduard)
* cut test_btf_attrs down to bare API calls plus VALIDATE_RAW_BTF,
with no BTF_*_ENC macros (sashiko, Eduard)
v1: https://lore.kernel.org/bpf/20260917012037.1396254-1-ihor.solodrai@linux.dev/
---
Ihor Solodrai (5):
libbpf: Walk types in btf_dump_resize(), not in mark_referenced()
libbpf: Render decl_tags in btf_dump
selftests/bpf: Add ASSERT_TEXT_EQ()
selftests/bpf: Test btf_dump rendering of decl_tags
selftests/bpf: Show the tag kflag in the raw BTF dump helper
tools/lib/bpf/btf_dump.c | 214 +++++++++----
tools/testing/selftests/bpf/btf_helpers.c | 10 +-
.../bpf/prog_tests/bpftool_btf_dump.c | 6 +-
.../selftests/bpf/prog_tests/btf_dump.c | 285 +++++++++++++++++-
.../selftests/bpf/prog_tests/btf_write.c | 34 +++
tools/testing/selftests/bpf/test_progs.h | 3 +
6 files changed, 475 insertions(+), 77 deletions(-)
base-commit: cc6010e6e5cbe1f743d3011991041f227d976a5e
--
2.50.1
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH bpf-next v2 1/5] libbpf: Walk types in btf_dump_resize(), not in mark_referenced() 2026-09-29 23:46 [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump Ihor Solodrai @ 2026-09-29 23:46 ` Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 2/5] libbpf: Render decl_tags in btf_dump Ihor Solodrai ` (4 subsequent siblings) 5 siblings, 0 replies; 7+ messages in thread From: Ihor Solodrai @ 2026-09-29 23:46 UTC (permalink / raw) To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann, Eduard Zingerman, Kumar Kartikeya Dwivedi Cc: bpf, Alan Maguire btf_dump_mark_referenced() both walks every type added since the last resize and decides what each one references. Move the walk out to btf_dump_resize() and hand the function one type at a time, so a second per-type job can share the same pass instead of adding another one. No functional change. Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev> Acked-by: Eduard Zingerman <eddyz87@gmail.com> --- tools/lib/bpf/btf_dump.c | 122 +++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 62 deletions(-) diff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c index fa995c02a170..e0293e6d3492 100644 --- a/tools/lib/bpf/btf_dump.c +++ b/tools/lib/bpf/btf_dump.c @@ -143,7 +143,7 @@ static void btf_dump_printf(const struct btf_dump *d, const char *fmt, ...) va_end(args); } -static int btf_dump_mark_referenced(struct btf_dump *d); +static int btf_dump_mark_referenced(struct btf_dump *d, const struct btf_type *t); static int btf_dump_resize(struct btf_dump *d); struct btf_dump *btf_dump__new(const struct btf *btf, @@ -195,6 +195,8 @@ struct btf_dump *btf_dump__new(const struct btf *btf, static int btf_dump_resize(struct btf_dump *d) { int err, last_id = btf__type_cnt(d->btf) - 1; + const struct btf_type *t; + __u32 i; if (last_id <= d->last_id) return 0; @@ -212,10 +214,13 @@ static int btf_dump_resize(struct btf_dump *d) d->type_states[0].emit_state = EMITTED; } - /* eagerly determine referenced types for anon enums */ - err = btf_dump_mark_referenced(d); - if (err) - return err; + for (i = d->last_id + 1; i <= last_id; i++) { + t = btf__type_by_id(d->btf, i); + + err = btf_dump_mark_referenced(d, t); + if (err) + return err; + } d->last_id = last_id; return 0; @@ -312,71 +317,64 @@ int btf_dump__dump_type(struct btf_dump *d, __u32 id) * top-level anonymous enum won't be referenced by anything, while embedded * one will. */ -static int btf_dump_mark_referenced(struct btf_dump *d) +static int btf_dump_mark_referenced(struct btf_dump *d, const struct btf_type *t) { - int i, j, n = btf__type_cnt(d->btf); - const struct btf_type *t; - __u32 vlen; + __u32 j, vlen = btf_vlen(t); - for (i = d->last_id + 1; i < n; i++) { - t = btf__type_by_id(d->btf, i); - vlen = btf_vlen(t); - - switch (btf_kind(t)) { - case BTF_KIND_INT: - case BTF_KIND_ENUM: - case BTF_KIND_ENUM64: - case BTF_KIND_FWD: - case BTF_KIND_FLOAT: - case BTF_KIND_LOC_PARAM: - case BTF_KIND_LOC_PROTO: - case BTF_KIND_LOCSEC: - break; + switch (btf_kind(t)) { + case BTF_KIND_INT: + case BTF_KIND_ENUM: + case BTF_KIND_ENUM64: + case BTF_KIND_FWD: + case BTF_KIND_FLOAT: + case BTF_KIND_LOC_PARAM: + case BTF_KIND_LOC_PROTO: + case BTF_KIND_LOCSEC: + break; - case BTF_KIND_VOLATILE: - case BTF_KIND_CONST: - case BTF_KIND_RESTRICT: - case BTF_KIND_PTR: - case BTF_KIND_TYPEDEF: - case BTF_KIND_FUNC: - case BTF_KIND_VAR: - case BTF_KIND_DECL_TAG: - case BTF_KIND_TYPE_TAG: - d->type_states[t->type].referenced = 1; - break; + case BTF_KIND_VOLATILE: + case BTF_KIND_CONST: + case BTF_KIND_RESTRICT: + case BTF_KIND_PTR: + case BTF_KIND_TYPEDEF: + case BTF_KIND_FUNC: + case BTF_KIND_VAR: + case BTF_KIND_DECL_TAG: + case BTF_KIND_TYPE_TAG: + d->type_states[t->type].referenced = 1; + break; - case BTF_KIND_ARRAY: { - const struct btf_array *a = btf_array(t); + case BTF_KIND_ARRAY: { + const struct btf_array *a = btf_array(t); - d->type_states[a->index_type].referenced = 1; - d->type_states[a->type].referenced = 1; - break; - } - case BTF_KIND_STRUCT: - case BTF_KIND_UNION: { - const struct btf_member *m = btf_members(t); + d->type_states[a->index_type].referenced = 1; + d->type_states[a->type].referenced = 1; + break; + } + case BTF_KIND_STRUCT: + case BTF_KIND_UNION: { + const struct btf_member *m = btf_members(t); - for (j = 0; j < vlen; j++, m++) - d->type_states[m->type].referenced = 1; - break; - } - case BTF_KIND_FUNC_PROTO: { - const struct btf_param *p = btf_params(t); + for (j = 0; j < vlen; j++, m++) + d->type_states[m->type].referenced = 1; + break; + } + case BTF_KIND_FUNC_PROTO: { + const struct btf_param *p = btf_params(t); - for (j = 0; j < vlen; j++, p++) - d->type_states[p->type].referenced = 1; - break; - } - case BTF_KIND_DATASEC: { - const struct btf_var_secinfo *v = btf_var_secinfos(t); + for (j = 0; j < vlen; j++, p++) + d->type_states[p->type].referenced = 1; + break; + } + case BTF_KIND_DATASEC: { + const struct btf_var_secinfo *v = btf_var_secinfos(t); - for (j = 0; j < vlen; j++, v++) - d->type_states[v->type].referenced = 1; - break; - } - default: - return -EINVAL; - } + for (j = 0; j < vlen; j++, v++) + d->type_states[v->type].referenced = 1; + break; + } + default: + return -EINVAL; } return 0; } -- 2.50.1 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH bpf-next v2 2/5] libbpf: Render decl_tags in btf_dump 2026-09-29 23:46 [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 1/5] libbpf: Walk types in btf_dump_resize(), not in mark_referenced() Ihor Solodrai @ 2026-09-29 23:46 ` Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 3/5] selftests/bpf: Add ASSERT_TEXT_EQ() Ihor Solodrai ` (3 subsequent siblings) 5 siblings, 0 replies; 7+ messages in thread From: Ihor Solodrai @ 2026-09-29 23:46 UTC (permalink / raw) To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann, Eduard Zingerman, Kumar Kartikeya Dwivedi Cc: bpf, Alan Maguire BTF_KIND_DECL_TAG is an attribute on a declaration: with kind_flag=0 the tag name is the argument of a btf_decl_tag(), and with kind_flag=1 it encodes a complete __attribute__. When doing btf_dump, render both forms at every declaration btf_dump emits - a record, a record member and a typedef: struct foo { ... } __attribute__((bar)); struct foo { int a __attribute__((bar)); }; typedef struct { ... } __attribute__((bar)) foo_t; A decl tag is a standalone type pointing at its target. Build an index of decl tags in btf_dump_resize(). Keep it as a flat array sorted by (target ID, tag ID). This allows for a stable emission order in btf_dump_emit_decl_tags(). btf_dump_emit_decl_tags() binary searches for where the target's entries would begin and walks tags while the target matches. A record shares its target with its members, so the component_idx is matched there. A record attribute goes after the closing brace, where __attribute__((packed)) already goes. A member attribute goes after the bit-field width: clang rejects one between the declarator and the ':'. A typedef takes it after the declarator, so a typedef of an anonymous record can carry two groups at once, one binding to the record and one to the typedef. This closes a generic gap in btf_dump, which silently dropped every decl tag it was given. Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev> Acked-by: Eduard Zingerman <eddyz87@gmail.com> --- tools/lib/bpf/btf_dump.c | 94 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c index e0293e6d3492..996e61b5f11f 100644 --- a/tools/lib/bpf/btf_dump.c +++ b/tools/lib/bpf/btf_dump.c @@ -77,6 +77,11 @@ struct btf_dump_data { bool is_array_char; }; +struct decl_tag_desc { + __u32 target_id; + __u32 tag_id; +}; + struct btf_dump { const struct btf *btf; btf_dump_printf_fn_t printf_fn; @@ -93,6 +98,11 @@ struct btf_dump { const char **cached_names; size_t cached_names_cap; + /* decl tags, sorted by (target ID, tag ID) */ + struct decl_tag_desc *decl_tags; + size_t decl_tag_cnt; + size_t decl_tag_cap; + /* topo-sorted list of dependent type definitions */ __u32 *emit_queue; int emit_queue_cap; @@ -192,9 +202,34 @@ struct btf_dump *btf_dump__new(const struct btf *btf, return libbpf_err_ptr(err); } +static int btf_dump_cmp_decl_tags(const void *a, const void *b) +{ + const struct decl_tag_desc *x = a, *y = b; + + if (x->target_id != y->target_id) + return x->target_id < y->target_id ? -1 : 1; + if (x->tag_id != y->tag_id) + return x->tag_id < y->tag_id ? -1 : 1; + return 0; +} + +static int btf_dump_push_decl_tag(struct btf_dump *d, __u32 id, const struct btf_type *t) +{ + if (libbpf_ensure_mem((void **)&d->decl_tags, &d->decl_tag_cap, + sizeof(*d->decl_tags), d->decl_tag_cnt + 1)) + return -ENOMEM; + + d->decl_tags[d->decl_tag_cnt++] = (struct decl_tag_desc) { + .target_id = t->type, + .tag_id = id, + }; + return 0; +} + static int btf_dump_resize(struct btf_dump *d) { int err, last_id = btf__type_cnt(d->btf) - 1; + size_t cnt = d->decl_tag_cnt; const struct btf_type *t; __u32 i; @@ -219,11 +254,25 @@ static int btf_dump_resize(struct btf_dump *d) err = btf_dump_mark_referenced(d, t); if (err) - return err; + goto out_err; + + if (btf_is_decl_tag(t)) { + err = btf_dump_push_decl_tag(d, i, t); + if (err) + goto out_err; + } } + if (d->decl_tag_cnt != cnt) + qsort(d->decl_tags, d->decl_tag_cnt, sizeof(*d->decl_tags), + btf_dump_cmp_decl_tags); + d->last_id = last_id; return 0; + +out_err: + d->decl_tag_cnt = cnt; + return err; } static void btf_dump_free_names(struct hashmap *map) @@ -256,6 +305,7 @@ void btf_dump__free(struct btf_dump *d) } } free(d->cached_names); + free(d->decl_tags); free(d->emit_queue); free(d->decl_stack); btf_dump_free_names(d->type_names); @@ -968,6 +1018,44 @@ static void btf_dump_emit_struct_fwd(struct btf_dump *d, __u32 id, btf_dump_type_name(d, id)); } +static void btf_dump_emit_decl_tag(struct btf_dump *d, const struct btf_type *t) +{ + const char *name = btf_name_of(d, t->name_off); + + if (btf_kflag(t)) + btf_dump_printf(d, " __attribute__((%s))", name); + else + btf_dump_printf(d, " __attribute__((btf_decl_tag(\"%s\")))", name); +} + +/* + * btf_dump_resize() keeps d->decl_tags sorted by (target ID, tag ID), so the + * tags of one type form a run that binary search finds the start of, in a + * fixed order so that the same BTF always renders the same C. + */ +static void btf_dump_emit_decl_tags(struct btf_dump *d, __u32 id, int comp_idx) +{ + size_t i, lo = 0, hi = d->decl_tag_cnt, mid; + const struct btf_type *t; + + /* lower bound on target_id */ + while (lo < hi) { + mid = lo + (hi - lo) / 2; + + if (d->decl_tags[mid].target_id < id) + lo = mid + 1; + else + hi = mid; + } + + for (i = lo; i < d->decl_tag_cnt && d->decl_tags[i].target_id == id; i++) { + t = btf__type_by_id(d->btf, d->decl_tags[i].tag_id); + + if (btf_decl_tag(t)->component_idx == comp_idx) + btf_dump_emit_decl_tag(d, t); + } +} + static void btf_dump_emit_struct_def(struct btf_dump *d, __u32 id, const struct btf_type *t, @@ -1013,6 +1101,8 @@ static void btf_dump_emit_struct_def(struct btf_dump *d, prev_bitfield = false; } + /* after the bit-field width */ + btf_dump_emit_decl_tags(d, id, i); btf_dump_printf(d, ";"); } @@ -1032,6 +1122,7 @@ static void btf_dump_emit_struct_def(struct btf_dump *d, } if (packed) btf_dump_printf(d, " __attribute__((packed))"); + btf_dump_emit_decl_tags(d, id, -1); } static const char *missing_base_types[][2] = { @@ -1209,6 +1300,7 @@ static void btf_dump_emit_typedef_def(struct btf_dump *d, __u32 id, btf_dump_printf(d, "typedef "); btf_dump_emit_type_decl(d, t->type, name, lvl); + btf_dump_emit_decl_tags(d, id, -1); } static int btf_dump_push_decl_stack_id(struct btf_dump *d, __u32 id) -- 2.50.1 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH bpf-next v2 3/5] selftests/bpf: Add ASSERT_TEXT_EQ() 2026-09-29 23:46 [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 1/5] libbpf: Walk types in btf_dump_resize(), not in mark_referenced() Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 2/5] libbpf: Render decl_tags in btf_dump Ihor Solodrai @ 2026-09-29 23:46 ` Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 4/5] selftests/bpf: Test btf_dump rendering of decl_tags Ihor Solodrai ` (2 subsequent siblings) 5 siblings, 0 replies; 7+ messages in thread From: Ihor Solodrai @ 2026-09-29 23:46 UTC (permalink / raw) To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann, Eduard Zingerman, Kumar Kartikeya Dwivedi Cc: bpf, Alan Maguire Text comparisons in test_progs repeat the same two statements: compare_text_to_expected() to print the diff, then an assert to record the failure. ASSERT_OK() on the raw return also appends a stale errno to the failure output. Add ASSERT_TEXT_EQ() after ASSERT_HAS_SUBSTR(). The assert cannot move into compare_text_to_expected() itself: the helper is linked into standalone binaries without test__fail(), and the framework unit test checks the raw mismatch return. Convert the 3 test_progs call sites plus test_ctx__dump_and_compare(), so multi-line dumps fail with a diff; prog_tests_framework.c keeps calling the helper directly. Suggested-by: Eduard Zingerman <eddyz87@gmail.com> Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev> --- .../testing/selftests/bpf/prog_tests/bpftool_btf_dump.c | 6 ++---- tools/testing/selftests/bpf/prog_tests/btf_dump.c | 9 ++------- tools/testing/selftests/bpf/test_progs.h | 3 +++ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/tools/testing/selftests/bpf/prog_tests/bpftool_btf_dump.c b/tools/testing/selftests/bpf/prog_tests/bpftool_btf_dump.c index abc62958b6ed..534aa79a4278 100644 --- a/tools/testing/selftests/bpf/prog_tests/bpftool_btf_dump.c +++ b/tools/testing/selftests/bpf/prog_tests/bpftool_btf_dump.c @@ -207,7 +207,6 @@ static void test_dump(const char *btf_path, bool sorted) { const char *exp_path; char *dump, *exp; - int err; exp_path = sorted ? EXPECTED_SORTED : EXPECTED_UNSORTED; @@ -219,8 +218,7 @@ static void test_dump(const char *btf_path, bool sorted) if (!exp) goto out_dump; - err = compare_text_to_expected(dump, exp); - ASSERT_OK(err, sorted ? "cmp_sorted" : "cmp_unsorted"); + ASSERT_TEXT_EQ(dump, exp, sorted ? "cmp_sorted" : "cmp_unsorted"); free(exp); out_dump: @@ -260,7 +258,7 @@ static void test_loc_dump(const char *btf_path) if (!dump) return; - ASSERT_OK(compare_text_to_expected(dump, expected), "cmp_loc_dump"); + ASSERT_TEXT_EQ(dump, expected, "cmp_loc_dump"); free(dump); } diff --git a/tools/testing/selftests/bpf/prog_tests/btf_dump.c b/tools/testing/selftests/bpf/prog_tests/btf_dump.c index fe04a955d46c..838f7232f591 100644 --- a/tools/testing/selftests/bpf/prog_tests/btf_dump.c +++ b/tools/testing/selftests/bpf/prog_tests/btf_dump.c @@ -161,12 +161,7 @@ static int test_btf_dump_case(int n, struct btf_dump_test_case *t) goto done; } - /* - * 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(dump, expected); - ASSERT_EQ(err, 0, "compare_text_to_expected"); + ASSERT_TEXT_EQ(dump, expected, "compare_text_to_expected"); done: free(expected); @@ -224,7 +219,7 @@ 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); + ASSERT_TEXT_EQ(t->dump_buf, expected_output, message); } static void test_btf_dump_incremental(void) diff --git a/tools/testing/selftests/bpf/test_progs.h b/tools/testing/selftests/bpf/test_progs.h index ea493c477fbd..adc18f5eceb6 100644 --- a/tools/testing/selftests/bpf/test_progs.h +++ b/tools/testing/selftests/bpf/test_progs.h @@ -349,6 +349,9 @@ void hexdump(const char *prefix, const void *buf, size_t len); ___ok; \ }) +#define ASSERT_TEXT_EQ(actual, expected, name) \ + ASSERT_TRUE(compare_text_to_expected(actual, expected) == 0, name) + #define ASSERT_MEMEQ(actual, expected, len, name) ({ \ static int duration = 0; \ const void *__act = actual; \ -- 2.50.1 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH bpf-next v2 4/5] selftests/bpf: Test btf_dump rendering of decl_tags 2026-09-29 23:46 [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump Ihor Solodrai ` (2 preceding siblings ...) 2026-09-29 23:46 ` [PATCH bpf-next v2 3/5] selftests/bpf: Add ASSERT_TEXT_EQ() Ihor Solodrai @ 2026-09-29 23:46 ` Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 5/5] selftests/bpf: Show the tag kflag in the raw BTF dump helper Ihor Solodrai 2026-09-30 9:10 ` [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump patchwork-bot+netdevbpf 5 siblings, 0 replies; 7+ messages in thread From: Ihor Solodrai @ 2026-09-29 23:46 UTC (permalink / raw) To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann, Eduard Zingerman, Kumar Kartikeya Dwivedi Cc: bpf, Alan Maguire Cover the decl tag rendering the libbpf 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 records lose their attributes - attributes on a var and on a func, which btf_dump never declares The incremental subtest's second round adds an attribute to the new struct, pinning the later-walk indexing of appended types. A separate subtest 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. Another covers typedefs, including a typedef of an anonymous record carrying three groups at once, binding to the member, the record and the typedef. Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev> Acked-by: Eduard Zingerman <eddyz87@gmail.com> --- .../selftests/bpf/prog_tests/btf_dump.c | 276 +++++++++++++++++- 1 file changed, 275 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 838f7232f591..17fa44cb9c91 100644 --- a/tools/testing/selftests/bpf/prog_tests/btf_dump.c +++ b/tools/testing/selftests/bpf/prog_tests/btf_dump.c @@ -310,6 +310,8 @@ static void test_btf_dump_incremental(void) ASSERT_OK(err, "field_ok"); err = btf__add_field(btf, "s", 6, 64, 0); ASSERT_OK(err, "field_ok"); + /* attribute on a type added after the first dump: the later walk indexes it */ + ASSERT_EQ(btf__add_decl_attr(btf, "round2", 7, -1), 8, "round2_attr"); test_ctx__dump_and_compare(&t, "struct s___2 {\n" @@ -318,7 +320,270 @@ static void test_btf_dump_incremental(void) " Y___2 = 1,\n" " } y;\n" " struct s s;\n" -"};\n\n" , "c_dump1"); +"} __attribute__((round2));\n\n", "c_dump1"); + + 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 lookups land outside their runs and the records lose their + * attributes. + */ + 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); +} + +/* + * 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); } @@ -1104,6 +1369,15 @@ 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_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.50.1 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH bpf-next v2 5/5] selftests/bpf: Show the tag kflag in the raw BTF dump helper 2026-09-29 23:46 [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump Ihor Solodrai ` (3 preceding siblings ...) 2026-09-29 23:46 ` [PATCH bpf-next v2 4/5] selftests/bpf: Test btf_dump rendering of decl_tags Ihor Solodrai @ 2026-09-29 23:46 ` Ihor Solodrai 2026-09-30 9:10 ` [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump patchwork-bot+netdevbpf 5 siblings, 0 replies; 7+ messages in thread From: Ihor Solodrai @ 2026-09-29 23:46 UTC (permalink / raw) To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann, Eduard Zingerman, Kumar Kartikeya Dwivedi Cc: bpf, Alan Maguire A kflag on BTF_KIND_DECL_TAG or BTF_KIND_TYPE_TAG means the tag string is an __attribute__ list rather than a btf_decl_tag()/btf_type_tag() argument. fprintf_btf_type_raw() did not print it, so a tag added with btf__add_decl_tag() and an attribute added with btf__add_decl_attr() dumped identically apart from their type IDs. Print " kflag=1" for those two kinds, only when the flag is set, so no existing expected string changes. Add a btf_write subtest for btf__add_decl_attr() and btf__add_type_attr(). Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev> Acked-by: Eduard Zingerman <eddyz87@gmail.com> --- tools/testing/selftests/bpf/btf_helpers.c | 10 ++++-- .../selftests/bpf/prog_tests/btf_write.c | 34 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/tools/testing/selftests/bpf/btf_helpers.c b/tools/testing/selftests/bpf/btf_helpers.c index 85a4d15d1dd1..e04b61c08cd8 100644 --- a/tools/testing/selftests/bpf/btf_helpers.c +++ b/tools/testing/selftests/bpf/btf_helpers.c @@ -114,9 +114,12 @@ int fprintf_btf_type_raw(FILE *out, const struct btf *btf, __u32 id) case BTF_KIND_VOLATILE: case BTF_KIND_RESTRICT: case BTF_KIND_TYPEDEF: - case BTF_KIND_TYPE_TAG: fprintf(out, " type_id=%u", t->type); break; + case BTF_KIND_TYPE_TAG: + fprintf(out, " type_id=%u%s", t->type, + btf_kflag(t) ? " kflag=1" : ""); + break; case BTF_KIND_ARRAY: { const struct btf_array *arr = btf_array(t); @@ -203,8 +206,9 @@ int fprintf_btf_type_raw(FILE *out, const struct btf *btf, __u32 id) fprintf(out, " size=%u", t->size); break; case BTF_KIND_DECL_TAG: - fprintf(out, " type_id=%u component_idx=%d", - t->type, btf_decl_tag(t)->component_idx); + fprintf(out, " type_id=%u component_idx=%d%s", + t->type, btf_decl_tag(t)->component_idx, + btf_kflag(t) ? " kflag=1" : ""); break; case BTF_KIND_LOC_PARAM: { struct btf_loc_param *p = btf_loc_param(t); diff --git a/tools/testing/selftests/bpf/prog_tests/btf_write.c b/tools/testing/selftests/bpf/prog_tests/btf_write.c index 5c84723cf254..3bed3a28c06a 100644 --- a/tools/testing/selftests/bpf/prog_tests/btf_write.c +++ b/tools/testing/selftests/bpf/prog_tests/btf_write.c @@ -606,6 +606,38 @@ static void test_btf_add_btf_split() btf__free(base); } +static void test_btf_attrs(void) +{ + struct btf *btf; + + btf = btf__new_empty(); + if (!ASSERT_OK_PTR(btf, "new_empty")) + return; + + btf__add_int(btf, "int", 4, BTF_INT_SIGNED); + btf__add_struct(btf, "s", 4); + btf__add_field(btf, "f", 1, 0, 0); + + btf__add_decl_tag(btf, "tag", 2, -1); + btf__add_decl_attr(btf, "tag", 2, -1); + btf__add_decl_attr(btf, "member_attr", 2, 0); + btf__add_type_tag(btf, "tag", 1); + btf__add_type_attr(btf, "tag", 1); + + VALIDATE_RAW_BTF( + btf, + "[1] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED", + "[2] STRUCT 's' size=4 vlen=1\n" + "\t'f' type_id=1 bits_offset=0", + "[3] DECL_TAG 'tag' type_id=2 component_idx=-1", + "[4] DECL_TAG 'tag' type_id=2 component_idx=-1 kflag=1", + "[5] DECL_TAG 'member_attr' type_id=2 component_idx=0 kflag=1", + "[6] TYPE_TAG 'tag' type_id=1", + "[7] TYPE_TAG 'tag' type_id=1 kflag=1"); + + btf__free(btf); +} + void test_btf_write() { if (test__start_subtest("btf_add")) @@ -614,4 +646,6 @@ void test_btf_write() test_btf_add_btf(); if (test__start_subtest("btf_add_btf_split")) test_btf_add_btf_split(); + if (test__start_subtest("btf_attrs")) + test_btf_attrs(); } -- 2.50.1 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump 2026-09-29 23:46 [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump Ihor Solodrai ` (4 preceding siblings ...) 2026-09-29 23:46 ` [PATCH bpf-next v2 5/5] selftests/bpf: Show the tag kflag in the raw BTF dump helper Ihor Solodrai @ 2026-09-30 9:10 ` patchwork-bot+netdevbpf 5 siblings, 0 replies; 7+ messages in thread From: patchwork-bot+netdevbpf @ 2026-09-30 9:10 UTC (permalink / raw) To: Ihor Solodrai; +Cc: ast, andrii, daniel, eddyz87, memxor, bpf, alan.maguire Hello: This series was applied to bpf/bpf-next.git (master) by Alexei Starovoitov <ast@kernel.org>: On Tue, 29 Sep 2026 16:46:03 -0700 you wrote: > BTF supports arbitrary __attribute__ encoding via decl tags and type > tags [1]. However btf_dump facility only outputs the attributes > encoded in BTF as type tags, which was implemented in commit > 2019c58318b8 ("libbpf: Check the kflag of type tags in btf_dump"). > > BTF_KIND_DECL_TAG has been left out of btf_dump entirely, independent > of the kflag value. An attribute a BTF producer encoded is silently > dropped from the C output. > > [...] Here is the summary with links: - [bpf-next,v2,1/5] libbpf: Walk types in btf_dump_resize(), not in mark_referenced() https://git.kernel.org/bpf/bpf-next/c/1419795b98cf - [bpf-next,v2,2/5] libbpf: Render decl_tags in btf_dump https://git.kernel.org/bpf/bpf-next/c/76ea31239021 - [bpf-next,v2,3/5] selftests/bpf: Add ASSERT_TEXT_EQ() https://git.kernel.org/bpf/bpf-next/c/1c36a1a41a47 - [bpf-next,v2,4/5] selftests/bpf: Test btf_dump rendering of decl_tags https://git.kernel.org/bpf/bpf-next/c/096ee6e38267 - [bpf-next,v2,5/5] selftests/bpf: Show the tag kflag in the raw BTF dump helper https://git.kernel.org/bpf/bpf-next/c/2c964f6c5d8e You are awesome, thank you! -- Deet-doot-dot, I am a bot. https://korg.docs.kernel.org/patchwork/pwbot.html ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-30 9:10 UTC | newest] Thread overview: 7+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-09-29 23:46 [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 1/5] libbpf: Walk types in btf_dump_resize(), not in mark_referenced() Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 2/5] libbpf: Render decl_tags in btf_dump Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 3/5] selftests/bpf: Add ASSERT_TEXT_EQ() Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 4/5] selftests/bpf: Test btf_dump rendering of decl_tags Ihor Solodrai 2026-09-29 23:46 ` [PATCH bpf-next v2 5/5] selftests/bpf: Show the tag kflag in the raw BTF dump helper Ihor Solodrai 2026-09-30 9:10 ` [PATCH bpf-next v2 0/5] libbpf: Render decl_tags in btf_dump patchwork-bot+netdevbpf
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox