* [PATCH bpf-next v1 0/4] libbpf: Render decl_tags in btf_dump
@ 2026-09-17 1:20 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
` (3 more replies)
0 siblings, 4 replies; 18+ messages in thread
From: Ihor Solodrai @ 2026-09-17 1:20 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: bpf
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));
vmlinux.h generated in-tree remains unchanged: every decl tag in
vmlinux BTF is a tag on a FUNC, which is not supported by btf_dump.
[1] https://lore.kernel.org/bpf/20250130201239.1429648-1-ihor.solodrai@linux.dev/
---
The series consists of the following:
* patch #1 is a non-functional refactoring, preparing
btf_dump_resize() for decl_tag index build
* patch #2 implements the full decl_tag dump support
* patch #3 adds the selftests to cover the new feature
* patch #4 is an independent selftests change relevant to kflagged
decl_tag handling
Ihor Solodrai (4):
libbpf: Walk types in btf_dump_resize(), not in mark_referenced()
libbpf: Render decl_tags in btf_dump
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 | 210 ++++++++---
tools/testing/selftests/bpf/btf_helpers.c | 10 +-
.../selftests/bpf/prog_tests/btf_dump.c | 343 +++++++++++++++++-
.../selftests/bpf/prog_tests/btf_write.c | 60 +++
4 files changed, 560 insertions(+), 63 deletions(-)
base-commit: 10c4f610b215bf961235141161992f010cf7e451
--
2.55.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH bpf-next v1 1/4] libbpf: Walk types in btf_dump_resize(), not in mark_referenced()
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 ` 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
` (2 subsequent siblings)
3 siblings, 1 reply; 18+ messages in thread
From: Ihor Solodrai @ 2026-09-17 1:20 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: bpf
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>
---
tools/lib/bpf/btf_dump.c | 116 +++++++++++++++++++--------------------
1 file changed, 57 insertions(+), 59 deletions(-)
diff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c
index 123c448f20c7..635fa969e145 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,68 +317,61 @@ 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:
- 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:
+ 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.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH bpf-next v1 2/4] libbpf: Render decl_tags in btf_dump
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 1:20 ` Ihor Solodrai
2026-09-17 1:30 ` sashiko-bot
` (3 more replies)
2026-09-17 1:20 ` [PATCH bpf-next v1 3/4] selftests/bpf: Test btf_dump rendering of decl_tags Ihor Solodrai
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
3 siblings, 4 replies; 18+ messages in thread
From: Ihor Solodrai @ 2026-09-17 1:20 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: bpf
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(). Tag IDs are unique, so the comparison is
a total order and qsort not being stable does not matter.
Only composite and typedef targets are indexed. Valid BTF allows for
decl_tags on many types, however btf_dump only supports records,
record members and typedefs and ignores datasec, var and func.
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.
Every decl tag in vmlinux BTF is a bpf_kfunc or bpf_fastcall tag on a
FUNC, which has no declaration in the C output, so generated vmlinux.h
does not change.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
tools/lib/bpf/btf_dump.c | 94 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 94 insertions(+)
diff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c
index 635fa969e145..1e8236c8b9a5 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_tags_cnt;
+ size_t decl_tags_cap;
+
/* topo-sorted list of dependent type definitions */
__u32 *emit_queue;
int emit_queue_cap;
@@ -192,9 +202,37 @@ 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;
+ return x->tag_id < y->tag_id ? -1 : 1;
+}
+
+static int btf_dump_push_decl_tag(struct btf_dump *d, __u32 id, const struct btf_type *t)
+{
+ const struct btf_type *target = btf__type_by_id(d->btf, t->type);
+
+ if (!target || (!btf_is_composite(target) && !btf_is_typedef(target)))
+ return 0;
+
+ if (libbpf_ensure_mem((void **)&d->decl_tags, &d->decl_tags_cap,
+ sizeof(*d->decl_tags), d->decl_tags_cnt + 1))
+ return -ENOMEM;
+
+ d->decl_tags[d->decl_tags_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_tags_cnt;
const struct btf_type *t;
__u32 i;
@@ -220,8 +258,18 @@ static int btf_dump_resize(struct btf_dump *d)
err = btf_dump_mark_referenced(d, t);
if (err)
return err;
+
+ if (btf_is_decl_tag(t)) {
+ err = btf_dump_push_decl_tag(d, i, t);
+ if (err)
+ return err;
+ }
}
+ if (d->decl_tags_cnt != cnt)
+ qsort(d->decl_tags, d->decl_tags_cnt, sizeof(*d->decl_tags),
+ btf_dump_cmp_decl_tags);
+
d->last_id = last_id;
return 0;
}
@@ -256,6 +304,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);
@@ -962,6 +1011,47 @@ 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.
+ *
+ * component_idx is not stored in d->decl_tags: a record and its members share
+ * a target ID, so it is read from each tag.
+ */
+static void btf_dump_emit_decl_tags(struct btf_dump *d, __u32 id, int comp_idx)
+{
+ size_t i, lo = 0, hi = d->decl_tags_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_tags_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,
@@ -1007,6 +1097,8 @@ static void btf_dump_emit_struct_def(struct btf_dump *d,
prev_bitfield = false;
}
+ /* after the bit-field width; an attribute cannot precede it */
+ btf_dump_emit_decl_tags(d, id, i);
btf_dump_printf(d, ";");
}
@@ -1026,6 +1118,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] = {
@@ -1203,6 +1296,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.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH bpf-next v1 3/4] selftests/bpf: Test btf_dump rendering of decl_tags
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 1:20 ` [PATCH bpf-next v1 2/4] libbpf: Render decl_tags in btf_dump Ihor Solodrai
@ 2026-09-17 1:20 ` Ihor Solodrai
2026-09-17 10:30 ` Alan Maguire
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
3 siblings, 2 replies; 18+ messages in thread
From: Ihor Solodrai @ 2026-09-17 1:20 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: bpf
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
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH bpf-next v1 4/4] selftests/bpf: Show the tag kflag in the raw BTF dump helper
2026-09-17 1:20 [PATCH bpf-next v1 0/4] libbpf: Render decl_tags in btf_dump Ihor Solodrai
` (2 preceding siblings ...)
2026-09-17 1:20 ` [PATCH bpf-next v1 3/4] selftests/bpf: Test btf_dump rendering of decl_tags Ihor Solodrai
@ 2026-09-17 1:20 ` Ihor Solodrai
2026-09-17 1:27 ` sashiko-bot
2026-09-17 23:21 ` Eduard Zingerman
3 siblings, 2 replies; 18+ messages in thread
From: Ihor Solodrai @ 2026-09-17 1:20 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: bpf
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>
---
tools/testing/selftests/bpf/btf_helpers.c | 10 +++-
.../selftests/bpf/prog_tests/btf_write.c | 60 +++++++++++++++++++
2 files changed, 67 insertions(+), 3 deletions(-)
diff --git a/tools/testing/selftests/bpf/btf_helpers.c b/tools/testing/selftests/bpf/btf_helpers.c
index 1c1c2c26690a..6956dffb4f56 100644
--- a/tools/testing/selftests/bpf/btf_helpers.c
+++ b/tools/testing/selftests/bpf/btf_helpers.c
@@ -111,9 +111,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);
@@ -200,8 +203,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;
default:
break;
diff --git a/tools/testing/selftests/bpf/prog_tests/btf_write.c b/tools/testing/selftests/bpf/prog_tests/btf_write.c
index 5c84723cf254..7ddda9348e4f 100644
--- a/tools/testing/selftests/bpf/prog_tests/btf_write.c
+++ b/tools/testing/selftests/bpf/prog_tests/btf_write.c
@@ -606,6 +606,64 @@ static void test_btf_add_btf_split()
btf__free(base);
}
+static void test_btf_attrs(void)
+{
+ const struct btf_type *t;
+ struct btf *btf;
+ int id;
+
+ btf = btf__new_empty();
+ if (!ASSERT_OK_PTR(btf, "new_empty"))
+ return;
+
+ id = btf__add_int(btf, "int", 4, BTF_INT_SIGNED);
+ ASSERT_EQ(id, 1, "int_id");
+
+ id = btf__add_struct(btf, "s", 4);
+ ASSERT_EQ(id, 2, "struct_id");
+ ASSERT_OK(btf__add_field(btf, "f", 1, 0, 0), "field_ok");
+
+ id = btf__add_decl_tag(btf, "tag", 2, -1);
+ ASSERT_EQ(id, 3, "decl_tag_id");
+ t = btf__type_by_id(btf, 3);
+ ASSERT_EQ(btf_kind(t), BTF_KIND_DECL_TAG, "decl_tag_kind");
+ ASSERT_EQ(btf_kflag(t), 0, "decl_tag_kflag");
+
+ id = btf__add_decl_attr(btf, "tag", 2, -1);
+ ASSERT_EQ(id, 4, "decl_attr_id");
+ t = btf__type_by_id(btf, 4);
+ ASSERT_EQ(btf_kind(t), BTF_KIND_DECL_TAG, "decl_attr_kind");
+ ASSERT_EQ(btf_kflag(t), 1, "decl_attr_kflag");
+ ASSERT_EQ(t->type, 2, "decl_attr_type");
+ ASSERT_EQ(btf_decl_tag(t)->component_idx, -1, "decl_attr_component_idx");
+
+ id = btf__add_decl_attr(btf, "member_attr", 2, 0);
+ ASSERT_EQ(id, 5, "member_attr_id");
+ ASSERT_EQ(btf_decl_tag(btf__type_by_id(btf, 5))->component_idx, 0,
+ "member_attr_component_idx");
+
+ id = btf__add_type_tag(btf, "tag", 1);
+ ASSERT_EQ(id, 6, "type_tag_id");
+ ASSERT_EQ(btf_kflag(btf__type_by_id(btf, 6)), 0, "type_tag_kflag");
+
+ id = btf__add_type_attr(btf, "tag", 1);
+ ASSERT_EQ(id, 7, "type_attr_id");
+ ASSERT_EQ(btf_kflag(btf__type_by_id(btf, 7)), 1, "type_attr_kflag");
+
+ 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 +672,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.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 4/4] selftests/bpf: Show the tag kflag in the raw BTF dump helper
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
1 sibling, 0 replies; 18+ messages in thread
From: sashiko-bot @ 2026-09-17 1:27 UTC (permalink / raw)
To: Ihor Solodrai; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] bpf: NULL pointer dereference in test_btf_attrs on test failure
--
commit 551463268433576dd9cc5cc27a8ffb06a64ecabb
Author: Ihor Solodrai <ihor.solodrai@linux.dev>
selftests/bpf: Show the tag kflag in the raw BTF dump helper
This commit updates fprintf_btf_type_raw() to print the kflag for
BTF_KIND_DECL_TAG and BTF_KIND_TYPE_TAG types. It also adds a new subtest
test_btf_attrs to prog_tests/btf_write.c to verify this behavior.
> diff --git a/tools/testing/selftests/bpf/prog_tests/btf_write.c b/tools/testing/selftests/bpf/prog_tests/btf_write.c
> index 5c84723cf2540..7ddda9348e4fe 100644
> --- a/tools/testing/selftests/bpf/prog_tests/btf_write.c
> +++ b/tools/testing/selftests/bpf/prog_tests/btf_write.c
> @@ -606,6 +606,64 @@ static void test_btf_add_btf_split()
[ ... ]
> +static void test_btf_attrs(void)
> +{
> + const struct btf_type *t;
> + struct btf *btf;
> + int id;
> +
> + btf = btf__new_empty();
> + if (!ASSERT_OK_PTR(btf, "new_empty"))
> + return;
> +
> + id = btf__add_int(btf, "int", 4, BTF_INT_SIGNED);
> + ASSERT_EQ(id, 1, "int_id");
> +
> + id = btf__add_struct(btf, "s", 4);
> + ASSERT_EQ(id, 2, "struct_id");
> + ASSERT_OK(btf__add_field(btf, "f", 1, 0, 0), "field_ok");
> +
> + id = btf__add_decl_tag(btf, "tag", 2, -1);
> + ASSERT_EQ(id, 3, "decl_tag_id");
> + t = btf__type_by_id(btf, 3);
> + ASSERT_EQ(btf_kind(t), BTF_KIND_DECL_TAG, "decl_tag_kind");
[Severity: Medium]
In BPF selftests, ASSERT_EQ() logs a failure but does not abort the execution
of the test block. If btf__add_decl_tag() fails, the subsequent call to
btf__type_by_id() will return a NULL pointer.
Will this cause a NULL pointer dereference when t is passed to btf_kind(),
which unconditionally dereferences it?
> + ASSERT_EQ(btf_kflag(t), 0, "decl_tag_kflag");
> +
> + id = btf__add_decl_attr(btf, "tag", 2, -1);
> + ASSERT_EQ(id, 4, "decl_attr_id");
> + t = btf__type_by_id(btf, 4);
> + ASSERT_EQ(btf_kind(t), BTF_KIND_DECL_TAG, "decl_attr_kind");
> + ASSERT_EQ(btf_kflag(t), 1, "decl_attr_kflag");
> + ASSERT_EQ(t->type, 2, "decl_attr_type");
> + ASSERT_EQ(btf_decl_tag(t)->component_idx, -1, "decl_attr_component_idx");
> +
> + id = btf__add_decl_attr(btf, "member_attr", 2, 0);
> + ASSERT_EQ(id, 5, "member_attr_id");
> + ASSERT_EQ(btf_decl_tag(btf__type_by_id(btf, 5))->component_idx, 0,
> + "member_attr_component_idx");
[Severity: Medium]
Similarly, could chaining a potential NULL return from btf__type_by_id()
directly into btf_decl_tag() lead to an out-of-bounds dereference due to
pointer arithmetic on NULL?
> +
> + id = btf__add_type_tag(btf, "tag", 1);
> + ASSERT_EQ(id, 6, "type_tag_id");
> + ASSERT_EQ(btf_kflag(btf__type_by_id(btf, 6)), 0, "type_tag_kflag");
[Severity: Medium]
Is it possible that calling btf_kflag() on a potential NULL pointer from
btf__type_by_id() results in a segmentation fault if the type addition failed?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260917012037.1396254-1-ihor.solodrai@linux.dev?part=4
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 2/4] libbpf: Render decl_tags in btf_dump
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
` (2 subsequent siblings)
3 siblings, 0 replies; 18+ messages in thread
From: sashiko-bot @ 2026-09-17 1:30 UTC (permalink / raw)
To: Ihor Solodrai; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] libbpf: btf_dump_cmp_decl_tags violates strict weak ordering in qsort
- [Medium] libbpf: duplicate tags and memory leak on btf_dump_resize error paths
--
commit 869ac84b9144d9ee48198dbc0d57c9510a6f9d98
Author: Ihor Solodrai <ihor.solodrai@linux.dev>
Subject: libbpf: Render decl_tags in btf_dump
This commit adds support for rendering BTF_KIND_DECL_TAG in btf_dump.
It builds an array of decl tags sorted by target ID and tag ID, which
are then emitted at every declaration.
> diff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c
> --- a/tools/lib/bpf/btf_dump.c
> +++ b/tools/lib/bpf/btf_dump.c
> @@ -202,9 +202,15 @@ struct btf_dump *btf_dump__new(const struct btf *btf,
> +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;
> + return x->tag_id < y->tag_id ? -1 : 1;
[Severity: Medium]
Does this comparator satisfy the strict weak ordering requirement for qsort?
If two elements have the same tag_id, this returns 1 instead of 0. Depending
on the libc implementation, returning 1 when comparing an element to itself
or to a duplicate could break antisymmetry and potentially lead to out of
bounds accesses or infinite loops.
> +}
[ ... ]
> @@ -258,8 +258,18 @@ static int btf_dump_resize(struct btf_dump *d)
> err = btf_dump_mark_referenced(d, t);
> if (err)
> return err;
[Severity: Medium]
Do we need to roll back d->decl_tags_cnt or free resources here?
If this loop encounters an error and returns early, it leaves the previously
pushed decl_tags in the array, but d->last_id isn't updated.
If a user handles the error and resumes using the btf_dump object, won't it
re-push those same tags on the next call, causing duplicates and memory leaks?
> +
> + if (btf_is_decl_tag(t)) {
> + err = btf_dump_push_decl_tag(d, i, t);
> + if (err)
> + return err;
[Severity: Medium]
Similarly, does returning early here lead to the same duplicate tag entries
on retry, since d->decl_tags_cnt is not rolled back on error?
> + }
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260917012037.1396254-1-ihor.solodrai@linux.dev?part=2
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 2/4] libbpf: Render decl_tags in btf_dump
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
3 siblings, 1 reply; 18+ messages in thread
From: Alexei Starovoitov @ 2026-09-17 2:12 UTC (permalink / raw)
To: Ihor Solodrai, Andrii Nakryiko, Daniel Borkmann, Eduard Zingerman,
Kumar Kartikeya Dwivedi
Cc: bpf
On Wed, Sep 16, 2026 at 06:20 PM Ihor Solodrai <ihor.solodrai@linux.dev> wrote:
> 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:
[...]
> Every decl tag in vmlinux BTF is a bpf_kfunc or bpf_fastcall tag on a
> FUNC, which has no declaration in the C output, so generated vmlinux.h
> does not change.
iirc the goal of this series is "bpftool btf dump file prog.bpf.o
format c" and skeletons growing
__attribute__((btf_decl_tag("contains:..."))) on members.
Pls say so in the commit log.
Otherwise it's 200 lines of code for "generated vmlinux.h doesn't
change".
> +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->tar
get_id)
> + return x->target_id < y->target_id ? -1 : 1;
> + return x->tag_id < y->tag_id ? -1 : 1;
> +}
nit: return 0 when equal. cmp(x, x) != 0 is asking for trouble.
> @@ -220,8 +258,18 @@ static int btf_dump_resize(struct btf_dump *d)
> err = btf_dump_mark_referenced(d, t);
> if (err)
> return err;
> +
> + if (btf_is_decl_tag(t)) {
> + err = btf_dump_push_decl_tag(d, i, t);
> + if (err)
> + return err;
> + }
> }
\edited robot voice
On either error d->last_id is not advanced but d->decl_tags_cnt keeps
whatever was pushed so far, so the next btf_dump__dump_type() walks
the same ids again and pushes the same tags twice -> duplicated
__attribute__ in the output ?
pw-bot: cr
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 2/4] libbpf: Render decl_tags in btf_dump
2026-09-17 2:12 ` Alexei Starovoitov
@ 2026-09-17 3:25 ` Ihor Solodrai
0 siblings, 0 replies; 18+ messages in thread
From: Ihor Solodrai @ 2026-09-17 3:25 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: bpf
On 9/16/26 7:12 PM, Alexei Starovoitov wrote:
> On Wed, Sep 16, 2026 at 06:20 PM Ihor Solodrai <ihor.solodrai@linux.dev> wrote:
>> 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:
>
> [...]
>
>> Every decl tag in vmlinux BTF is a bpf_kfunc or bpf_fastcall tag on a
>> FUNC, which has no declaration in the C output, so generated vmlinux.h
>> does not change.
>
> iirc the goal of this series is "bpftool btf dump file prog.bpf.o
> format c" and skeletons growing
> __attribute__((btf_decl_tag("contains:..."))) on members.
> Pls say so in the commit log.
> Otherwise it's 200 lines of code for "generated vmlinux.h doesn't
> change".
Hi Alexei, thank you for quick review.
The decl_tags is an old generic gap in btf_dump, which gets closed
with this patch.
The "contains:" is not the use case I had in mind, but yes, it should
be covered as well. The next thing I planned after this lands is a
bpftool change to get rid of:
#pragma clang attribute push (__attribute__((preserve_access_index)), apply_to = record)
in vmlinux.h, by emitting the attributes explicitly per record.
It's been discussed a long time ago, but never picked up on libbpf /
bpftool side [1].
I'll add a paragraph describing potential use cases in v2.
[1] https://lore.kernel.org/bpf/20240503111836.25275-1-jose.marchesi@oracle.com/
>
>> +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->tar
> get_id)
>> + return x->target_id < y->target_id ? -1 : 1;
>> + return x->tag_id < y->tag_id ? -1 : 1;
>> +}
>
> nit: return 0 when equal. cmp(x, x) != 0 is asking for trouble.
For a valid deduped BTF we should never get a duplicate (target_id,
tag_id) pair. I'll see if there is a way to detect this and -EINVAL
>
>> @@ -220,8 +258,18 @@ static int btf_dump_resize(struct btf_dump *d)
>> err = btf_dump_mark_referenced(d, t);
>> if (err)
>> return err;
>> +
>> + if (btf_is_decl_tag(t)) {
>> + err = btf_dump_push_decl_tag(d, i, t);
>> + if (err)
>> + return err;
>> + }
>> }
>
> \edited robot voice
>
> On either error d->last_id is not advanced but d->decl_tags_cnt keeps
> whatever was pushed so far, so the next btf_dump__dump_type() walks
> the same ids again and pushes the same tags twice -> duplicated
> __attribute__ in the output ?
Yeah, my clanker flagged this too. I decided a mid-dump retry on error
is a weird case to support in the context of btf_dump, so ignored it.
Andrii, do you have an opinion?
>
> pw-bot: cr
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 3/4] selftests/bpf: Test btf_dump rendering of decl_tags
2026-09-17 1:20 ` [PATCH bpf-next v1 3/4] selftests/bpf: Test btf_dump rendering of decl_tags Ihor Solodrai
@ 2026-09-17 10:30 ` Alan Maguire
2026-09-17 17:38 ` Ihor Solodrai
2026-09-17 23:18 ` Eduard Zingerman
1 sibling, 1 reply; 18+ messages in thread
From: Alan Maguire @ 2026-09-17 10:30 UTC (permalink / raw)
To: Ihor Solodrai, Alexei Starovoitov, Andrii Nakryiko,
Daniel Borkmann, Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: bpf
On 17/09/2026 02:20, Ihor Solodrai wrote:
> 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>
Small suggestion; while this test is great at exercising the btf dump
internals, adding a bpftool btf dump test to prog_tests/bpftool_btf_dump.c
might help illustrate the change end-to-end (if you're planning this
as part of the work to eliminate the push/pop attributes feel free to
ignore). Thanks!
> ---
> .../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();
>
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 3/4] selftests/bpf: Test btf_dump rendering of decl_tags
2026-09-17 10:30 ` Alan Maguire
@ 2026-09-17 17:38 ` Ihor Solodrai
0 siblings, 0 replies; 18+ messages in thread
From: Ihor Solodrai @ 2026-09-17 17:38 UTC (permalink / raw)
To: Alan Maguire, Alexei Starovoitov, Andrii Nakryiko,
Daniel Borkmann, Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: bpf
On 9/17/26 3:30 AM, Alan Maguire wrote:
> On 17/09/2026 02:20, Ihor Solodrai wrote:
>> 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>
>
> Small suggestion; while this test is great at exercising the btf dump
> internals, adding a bpftool btf dump test to prog_tests/bpftool_btf_dump.c
> might help illustrate the change end-to-end (if you're planning this
> as part of the work to eliminate the push/pop attributes feel free to
> ignore). Thanks!
Hi Alan. Thanks for taking a look.
Yes, that's exactly the plan. btf_dump.c contains tests for libbpf btf_dump
primitives, and bpftool_btf_dump.c is for bpftool format c.
So more tests coming soon, no worries.
>
>
>> ---
>> .../selftests/bpf/prog_tests/btf_dump.c | 343 +++++++++++++++++-
>> 1 file changed, 342 insertions(+), 1 deletion(-)
>>
>> [...]
>>
>
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 1/4] libbpf: Walk types in btf_dump_resize(), not in mark_referenced()
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
0 siblings, 0 replies; 18+ messages in thread
From: Eduard Zingerman @ 2026-09-17 22:32 UTC (permalink / raw)
To: Ihor Solodrai, Alexei Starovoitov, Andrii Nakryiko,
Daniel Borkmann, Kumar Kartikeya Dwivedi
Cc: bpf
On Wed, 2026-09-16 at 18:20 -0700, Ihor Solodrai wrote:
> 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>
...
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 2/4] libbpf: Render decl_tags in btf_dump
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 22:46 ` Eduard Zingerman
2026-09-17 23:59 ` Andrii Nakryiko
3 siblings, 0 replies; 18+ messages in thread
From: Eduard Zingerman @ 2026-09-17 22:46 UTC (permalink / raw)
To: Ihor Solodrai, Alexei Starovoitov, Andrii Nakryiko,
Daniel Borkmann, Kumar Kartikeya Dwivedi
Cc: bpf
On Wed, 2026-09-16 at 18:20 -0700, Ihor Solodrai wrote:
> 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(). Tag IDs are unique, so the comparison is
> a total order and qsort not being stable does not matter.
>
> Only composite and typedef targets are indexed. Valid BTF allows for
> decl_tags on many types, however btf_dump only supports records,
> record members and typedefs and ignores datasec, var and func.
>
> 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.
>
> Every decl tag in vmlinux BTF is a bpf_kfunc or bpf_fastcall tag on a
> FUNC, which has no declaration in the C output, so generated vmlinux.h
> does not change.
>
> Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> ---
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
> @@ -962,6 +1011,47 @@ 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.
> + *
--- 8< ---
> + * component_idx is not stored in d->decl_tags: a record and its members share
> + * a target ID, so it is read from each tag.
Nit: useless detail, obvious from the code.
--- >8 ---
...
> @@ -1007,6 +1097,8 @@ static void btf_dump_emit_struct_def(struct btf_dump *d,
> prev_bitfield = false;
> }
>
> + /* after the bit-field width; an attribute cannot precede it */
Nit: /* after the bit-field width */ ?
> + btf_dump_emit_decl_tags(d, id, i);
> btf_dump_printf(d, ";");
> }
>
...
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 3/4] selftests/bpf: Test btf_dump rendering of decl_tags
2026-09-17 1:20 ` [PATCH bpf-next v1 3/4] selftests/bpf: Test btf_dump rendering of decl_tags Ihor Solodrai
2026-09-17 10:30 ` Alan Maguire
@ 2026-09-17 23:18 ` Eduard Zingerman
1 sibling, 0 replies; 18+ messages in thread
From: Eduard Zingerman @ 2026-09-17 23:18 UTC (permalink / raw)
To: Ihor Solodrai, Alexei Starovoitov, Andrii Nakryiko,
Daniel Borkmann, Kumar Kartikeya Dwivedi
Cc: bpf
On Wed, 2026-09-16 at 18:20 -0700, Ihor Solodrai wrote:
> 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>
> ---
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
> .../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);
Nit: Every caller of the compare_text_to_expected does such ASSERT,
let's embed the assert in the function and avoid the above comment.
...
> +/*
> + * 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)
Nit: I'd drop this test or made it a part of an existing one (w/o the s3 part).
...
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 4/4] selftests/bpf: Show the tag kflag in the raw BTF dump helper
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
1 sibling, 0 replies; 18+ messages in thread
From: Eduard Zingerman @ 2026-09-17 23:21 UTC (permalink / raw)
To: Ihor Solodrai, Alexei Starovoitov, Andrii Nakryiko,
Daniel Borkmann, Kumar Kartikeya Dwivedi
Cc: bpf
On Wed, 2026-09-16 at 18:20 -0700, Ihor Solodrai wrote:
> 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>
> diff --git a/tools/testing/selftests/bpf/prog_tests/btf_write.c b/tools/testing/selftests/bpf/prog_tests/btf_write.c
> index 5c84723cf254..7ddda9348e4f 100644
> --- a/tools/testing/selftests/bpf/prog_tests/btf_write.c
> +++ b/tools/testing/selftests/bpf/prog_tests/btf_write.c
> @@ -606,6 +606,64 @@ static void test_btf_add_btf_split()
> btf__free(base);
> }
>
> +static void test_btf_attrs(void)
> +{
> + const struct btf_type *t;
> + struct btf *btf;
> + int id;
> +
> + btf = btf__new_empty();
> + if (!ASSERT_OK_PTR(btf, "new_empty"))
> + return;
> +
> + id = btf__add_int(btf, "int", 4, BTF_INT_SIGNED);
> + ASSERT_EQ(id, 1, "int_id");
Note: we have a set of utility macro BTF_*_ENC macros in test_btf.h,
these are more convenient to use in some contexts.
(At-least there is no need to assert every time).
...
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 2/4] libbpf: Render decl_tags in btf_dump
2026-09-17 1:20 ` [PATCH bpf-next v1 2/4] libbpf: Render decl_tags in btf_dump Ihor Solodrai
` (2 preceding siblings ...)
2026-09-17 22:46 ` Eduard Zingerman
@ 2026-09-17 23:59 ` Andrii Nakryiko
2026-09-22 16:59 ` Ihor Solodrai
3 siblings, 1 reply; 18+ messages in thread
From: Andrii Nakryiko @ 2026-09-17 23:59 UTC (permalink / raw)
To: Ihor Solodrai
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi, bpf
On Wed, Sep 16, 2026 at 6:21 PM Ihor Solodrai <ihor.solodrai@linux.dev> wrote:
>
> 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(). Tag IDs are unique, so the comparison is
> a total order and qsort not being stable does not matter.
>
> Only composite and typedef targets are indexed. Valid BTF allows for
> decl_tags on many types, however btf_dump only supports records,
> record members and typedefs and ignores datasec, var and func.
>
> 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.
>
> Every decl tag in vmlinux BTF is a bpf_kfunc or bpf_fastcall tag on a
> FUNC, which has no declaration in the C output, so generated vmlinux.h
> does not change.
>
> Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
> ---
> tools/lib/bpf/btf_dump.c | 94 ++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 94 insertions(+)
>
series looks good, just small nits below
> diff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c
> index 635fa969e145..1e8236c8b9a5 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_tags_cnt;
> + size_t decl_tags_cap;
nit: decl_tag_cnt, decl_tag_cap, "count" expects singular noun before
it: "table count" not "tables count"
> +
> /* topo-sorted list of dependent type definitions */
> __u32 *emit_queue;
> int emit_queue_cap;
> @@ -192,9 +202,37 @@ 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;
> + return x->tag_id < y->tag_id ? -1 : 1;
> +}
> +
> +static int btf_dump_push_decl_tag(struct btf_dump *d, __u32 id, const struct btf_type *t)
> +{
> + const struct btf_type *target = btf__type_by_id(d->btf, t->type);
> +
> + if (!target || (!btf_is_composite(target) && !btf_is_typedef(target)))
target shouldn't be null with correct t->type, don't add defensive checks
but also why this artificial limitation on the kind of thing that is
tagged? memory savings?
> + return 0;
> +
> + if (libbpf_ensure_mem((void **)&d->decl_tags, &d->decl_tags_cap,
> + sizeof(*d->decl_tags), d->decl_tags_cnt + 1))
> + return -ENOMEM;
> +
> + d->decl_tags[d->decl_tags_cnt++] = (struct decl_tag_desc) {
> + .target_id = t->type,
> + .tag_id = id,
> + };
> + return 0;
> +}
> +
[...]
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 2/4] libbpf: Render decl_tags in btf_dump
2026-09-17 23:59 ` Andrii Nakryiko
@ 2026-09-22 16:59 ` Ihor Solodrai
2026-09-22 19:43 ` Andrii Nakryiko
0 siblings, 1 reply; 18+ messages in thread
From: Ihor Solodrai @ 2026-09-22 16:59 UTC (permalink / raw)
To: Andrii Nakryiko
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi, bpf
On 9/17/26 4:59 PM, Andrii Nakryiko wrote:
> On Wed, Sep 16, 2026 at 6:21 PM Ihor Solodrai <ihor.solodrai@linux.dev> wrote:
>>
>> [...]
>> ---
>> tools/lib/bpf/btf_dump.c | 94 ++++++++++++++++++++++++++++++++++++++++
>> 1 file changed, 94 insertions(+)
>>
>
> series looks good, just small nits below
Hi Andrii, thanks for the review.
Sorry I didn't reply earlier, wanted to get that REF_TYPE_FRAME stuff out.
>
>> diff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c
>> index 635fa969e145..1e8236c8b9a5 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_tags_cnt;
>> + size_t decl_tags_cap;
>
> nit: decl_tag_cnt, decl_tag_cap, "count" expects singular noun before
> it: "table count" not "tables count"
ack
>
>> +
>> /* topo-sorted list of dependent type definitions */
>> __u32 *emit_queue;
>> int emit_queue_cap;
>> @@ -192,9 +202,37 @@ 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;
>> + return x->tag_id < y->tag_id ? -1 : 1;
>> +}
>> +
>> +static int btf_dump_push_decl_tag(struct btf_dump *d, __u32 id, const struct btf_type *t)
>> +{
>> + const struct btf_type *target = btf__type_by_id(d->btf, t->type);
>> +
>> + if (!target || (!btf_is_composite(target) && !btf_is_typedef(target)))
>
> target shouldn't be null with correct t->type, don't add defensive checks
According to comments, decl_tags technically can refer to a type that
is not the BTF yet:
tools/lib/bpf/btf.c:3314-3323:
/*
* Append new BTF_KIND_DECL_TAG type with:
* - *value* - non-empty/non-NULL string;
* - *ref_type_id* - referenced type ID, it might not exist yet;
* ...
*/
int btf__add_decl_tag(...)
Granted, I wouldn't expect this to be a real use-case. But if we
decide to drop checks like this, then I think we should be explicit
about "no incremental dump", and maybe simplify relevant code a bit.
>
> but also why this artificial limitation on the kind of thing that is
> tagged? memory savings?
Yeah, I guess the effect of this is memory savings, but that wasn't
the motivation. I just thought "why collect tags that we are not
going to dump?". But the index can collect everything, it shouldn't
affect the dump. Do you think we should do that?
>
>
>> + return 0;
>> +
>> + if (libbpf_ensure_mem((void **)&d->decl_tags, &d->decl_tags_cap,
>> + sizeof(*d->decl_tags), d->decl_tags_cnt + 1))
>> + return -ENOMEM;
>> +
>> + d->decl_tags[d->decl_tags_cnt++] = (struct decl_tag_desc) {
>> + .target_id = t->type,
>> + .tag_id = id,
>> + };
>> + return 0;
>> +}
>> +
>
> [...]
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v1 2/4] libbpf: Render decl_tags in btf_dump
2026-09-22 16:59 ` Ihor Solodrai
@ 2026-09-22 19:43 ` Andrii Nakryiko
0 siblings, 0 replies; 18+ messages in thread
From: Andrii Nakryiko @ 2026-09-22 19:43 UTC (permalink / raw)
To: Ihor Solodrai
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi, bpf
On Tue, Sep 22, 2026 at 9:59 AM Ihor Solodrai <ihor.solodrai@linux.dev> wrote:
>
> On 9/17/26 4:59 PM, Andrii Nakryiko wrote:
> > On Wed, Sep 16, 2026 at 6:21 PM Ihor Solodrai <ihor.solodrai@linux.dev> wrote:
> >>
> >> [...]
> >> ---
> >> tools/lib/bpf/btf_dump.c | 94 ++++++++++++++++++++++++++++++++++++++++
> >> 1 file changed, 94 insertions(+)
> >>
> >
> > series looks good, just small nits below
>
> Hi Andrii, thanks for the review.
> Sorry I didn't reply earlier, wanted to get that REF_TYPE_FRAME stuff out.
>
> >
> >> diff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c
> >> index 635fa969e145..1e8236c8b9a5 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_tags_cnt;
> >> + size_t decl_tags_cap;
> >
> > nit: decl_tag_cnt, decl_tag_cap, "count" expects singular noun before
> > it: "table count" not "tables count"
>
> ack
>
> >
> >> +
> >> /* topo-sorted list of dependent type definitions */
> >> __u32 *emit_queue;
> >> int emit_queue_cap;
> >> @@ -192,9 +202,37 @@ 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;
> >> + return x->tag_id < y->tag_id ? -1 : 1;
> >> +}
> >> +
> >> +static int btf_dump_push_decl_tag(struct btf_dump *d, __u32 id, const struct btf_type *t)
> >> +{
> >> + const struct btf_type *target = btf__type_by_id(d->btf, t->type);
> >> +
> >> + if (!target || (!btf_is_composite(target) && !btf_is_typedef(target)))
> >
> > target shouldn't be null with correct t->type, don't add defensive checks
>
> According to comments, decl_tags technically can refer to a type that
> is not the BTF yet:
that means only during btf construction, I'm not sure how dumper
should behave if you pass it decl_tag that points to non-existing
type... erroring out would be one way, but silently ignoring is
probably not a right way anyways.
but as you said, I don't think anyone should pass half-constructed BTF
to dumper and expect that to work... incremental still means that any
point btf is correct and complete
>
> tools/lib/bpf/btf.c:3314-3323:
>
> /*
> * Append new BTF_KIND_DECL_TAG type with:
> * - *value* - non-empty/non-NULL string;
> * - *ref_type_id* - referenced type ID, it might not exist yet;
> * ...
> */
> int btf__add_decl_tag(...)
>
> Granted, I wouldn't expect this to be a real use-case. But if we
> decide to drop checks like this, then I think we should be explicit
> about "no incremental dump", and maybe simplify relevant code a bit.
>
> >
> > but also why this artificial limitation on the kind of thing that is
> > tagged? memory savings?
>
> Yeah, I guess the effect of this is memory savings, but that wasn't
> the motivation. I just thought "why collect tags that we are not
> going to dump?". But the index can collect everything, it shouldn't
> affect the dump. Do you think we should do that?
I'd collect everything to keep this part of logic complete and
forward-compatible without having to revisit it
>
>
> >
> >
> >> + return 0;
> >> +
> >> + if (libbpf_ensure_mem((void **)&d->decl_tags, &d->decl_tags_cap,
> >> + sizeof(*d->decl_tags), d->decl_tags_cnt + 1))
> >> + return -ENOMEM;
> >> +
> >> + d->decl_tags[d->decl_tags_cnt++] = (struct decl_tag_desc) {
> >> + .target_id = t->type,
> >> + .tag_id = id,
> >> + };
> >> + return 0;
> >> +}
> >> +
> >
> > [...]
>
^ permalink raw reply [flat|nested] 18+ messages in thread
end of thread, other threads:[~2026-09-22 19:43 UTC | newest]
Thread overview: 18+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH bpf-next v1 3/4] selftests/bpf: Test btf_dump rendering of decl_tags Ihor Solodrai
2026-09-17 10:30 ` 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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox