public inbox for bpf@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH v5 0/3] libbpf/bpftool: support merging split BTFs
@ 2026-02-24 16:40 Josef Bacik
  2026-02-24 16:40 ` [PATCH v5 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
                   ` (2 more replies)
  0 siblings, 3 replies; 8+ messages in thread
From: Josef Bacik @ 2026-02-24 16:40 UTC (permalink / raw)
  To: bpf

v1: https://lore.kernel.org/bpf/cover.1771605821.git.josef@toxicpanda.com/
v2: https://lore.kernel.org/bpf/cover.1771616227.git.josef@toxicpanda.com/
v3: https://lore.kernel.org/bpf/cover.1771622266.git.josef@toxicpanda.com/
v4: https://lore.kernel.org/bpf/cover.1771625484.git.josef@toxicpanda.com/

v1->v2:
- Added a btf__dedup() call to btf__add_btf() to ensure that we don't have
  duplicate types in the merged BTF.
v2->v3:
- AI review got confused about the UAF comment, so the comment was expanded to
  clarify the UAF potential.
- Fixed potential clobbering of errno in the error path.
v3->v4:
- Fixed a potential silent corruption pointed out by the AI review bot.
v4->v5:
- Addressed Andrii's comments for 1/3.
- Addressed Alan and Quentin's comments for 2/3.
- Addressed Alan's comments for 3/3.
- Added my Signed-off-by for the third patch.
- Made sure to validate everything still worked.

--- Original email ---

Hello,

I'm extending systing to do introspection on vfio devices, which requires having
the structs I need from the kernel available in userspace. Normally these are
loadable modules, but in the case of vfio there's multiple structs across
multiple modules. Normally you'd do the following to generate your vmlinux.h
with a module

bpftool btf dump file /sys/kernel/btf/<module> format c \
	--base /sys/kernel/btf/vmlinux > vmlinux.h

but if you need multiple modules you have to hack together multiple dumps and
merge them together. This patch series adds support for merging multiple BTF
sources together, so you can do

bpftool btf dump file /sys/kernel/btf/<module1> \
	file /sys/kernel/btf/<module2> format c \
	--base /sys/kernel/btf/vmlinux > vmlinux.h

I tested this with my usecase and it works. Thanks,

Josef

Josef Bacik (3):
  libbpf: support appending split BTF in btf__add_btf()
  bpftool: support merging multiple module BTFs in btf dump
  selftests/bpf: add test for btf__add_btf() with split BTF sources

 .../bpf/bpftool/Documentation/bpftool-btf.rst |  11 +-
 tools/bpf/bpftool/bash-completion/bpftool     |   6 +-
 tools/bpf/bpftool/btf.c                       | 128 ++++++++++++++++--
 tools/lib/bpf/btf.c                           |  37 +++--
 .../selftests/bpf/prog_tests/btf_write.c      | 111 +++++++++++++++
 5 files changed, 267 insertions(+), 26 deletions(-)

-- 
2.53.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v5 1/3] libbpf: support appending split BTF in btf__add_btf()
  2026-02-24 16:40 [PATCH v5 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
@ 2026-02-24 16:40 ` Josef Bacik
  2026-02-24 16:40 ` [PATCH v5 2/3] bpftool: support merging multiple module BTFs in btf dump Josef Bacik
  2026-02-24 16:40 ` [PATCH v5 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources Josef Bacik
  2 siblings, 0 replies; 8+ messages in thread
From: Josef Bacik @ 2026-02-24 16:40 UTC (permalink / raw)
  To: bpf

btf__add_btf() currently rejects split BTF sources with -ENOTSUP.
This prevents merging types from multiple kernel module BTFs that
are all split against the same vmlinux base.

Extend btf__add_btf() to handle split BTF sources by:

- Replacing the blanket -ENOTSUP with a validation that src and dst
  share the same base BTF pointer when both are split, returning
  -EOPNOTSUPP on mismatch.

- Computing src_start_id from the source's base to distinguish base
  type ID references (which must remain unchanged) from split type
  IDs (which must be remapped to new positions in the destination).

- Using src_btf->nr_types instead of btf__type_cnt()-1 for the type
  count, which is correct for both split and non-split sources.

- Skipping base string offsets (< start_str_off) during the string
  rewrite loop, mirroring the type ID skip pattern.  Since src and
  dst share the same base BTF, base string offsets are already valid
  and need no remapping.

For non-split sources the behavior is identical: src_start_id is 1,
the type_id < 1 guard is never true (VOID is already skipped), and
the remapping formula reduces to the original.  start_str_off is 0
so no string offsets are skipped.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
 tools/lib/bpf/btf.c | 37 +++++++++++++++++++++++++++++--------
 1 file changed, 29 insertions(+), 8 deletions(-)

diff --git a/tools/lib/bpf/btf.c b/tools/lib/bpf/btf.c
index 83fe79ffcb8f..be8cc4ee83fe 100644
--- a/tools/lib/bpf/btf.c
+++ b/tools/lib/bpf/btf.c
@@ -2004,12 +2004,18 @@ int btf__add_btf(struct btf *btf, const struct btf *src_btf)
 {
 	struct btf_pipe p = { .src = src_btf, .dst = btf };
 	int data_sz, sz, cnt, i, err, old_strs_len;
+	__u32 src_start_id;
 	__u32 *off;
 	void *t;
 
-	/* appending split BTF isn't supported yet */
-	if (src_btf->base_btf)
-		return libbpf_err(-ENOTSUP);
+	/* When appending split BTF, the destination must share the same base
+	 * BTF so that base type ID references remain valid.
+	 */
+	if (src_btf->base_btf &&
+	    src_btf->base_btf != btf->base_btf)
+		return libbpf_err(-EOPNOTSUPP);
+
+	src_start_id = src_btf->base_btf ? btf__type_cnt(src_btf->base_btf) : 1;
 
 	/* deconstruct BTF, if necessary, and invalidate raw_data */
 	if (btf_ensure_modifiable(btf))
@@ -2021,7 +2027,7 @@ int btf__add_btf(struct btf *btf, const struct btf *src_btf)
 	old_strs_len = btf->hdr->str_len;
 
 	data_sz = src_btf->hdr->type_len;
-	cnt = btf__type_cnt(src_btf) - 1;
+	cnt = src_btf->nr_types;
 
 	/* pre-allocate enough memory for new types */
 	t = btf_add_type_mem(btf, data_sz);
@@ -2060,6 +2066,16 @@ int btf__add_btf(struct btf *btf, const struct btf *src_btf)
 		if (err)
 			goto err_out;
 		while ((str_off = btf_field_iter_next(&it))) {
+			/* For split BTF sources, string offsets referencing
+			 * the base BTF (< start_str_off) remain unchanged
+			 * since dst shares the same base.  Only remap
+			 * strings in the split portion.  For non-split
+			 * sources start_str_off is 0 so all offsets except
+			 * the empty string (handled inside btf_rewrite_str)
+			 * are remapped as before.
+			 */
+			if (*str_off < src_btf->start_str_off)
+				continue;
 			err = btf_rewrite_str(&p, str_off);
 			if (err)
 				goto err_out;
@@ -2074,11 +2090,16 @@ int btf__add_btf(struct btf *btf, const struct btf *src_btf)
 			if (!*type_id) /* nothing to do for VOID references */
 				continue;
 
-			/* we haven't updated btf's type count yet, so
-			 * btf->start_id + btf->nr_types - 1 is the type ID offset we should
-			 * add to all newly added BTF types
+			/* For split BTF sources, type IDs referencing the
+			 * base BTF (< src_start_id) remain unchanged since
+			 * dst shares the same base.  Only remap IDs in the
+			 * split portion.  For non-split sources src_start_id
+			 * is 1 so all IDs are remapped as before.
 			 */
-			*type_id += btf->start_id + btf->nr_types - 1;
+			if (*type_id < src_start_id)
+				continue;
+
+			*type_id += btf->start_id + btf->nr_types - src_start_id;
 		}
 
 		/* go to next type data and type offset index entry */
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v5 2/3] bpftool: support merging multiple module BTFs in btf dump
  2026-02-24 16:40 [PATCH v5 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
  2026-02-24 16:40 ` [PATCH v5 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
@ 2026-02-24 16:40 ` Josef Bacik
  2026-02-24 17:18   ` bot+bpf-ci
  2026-02-24 16:40 ` [PATCH v5 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources Josef Bacik
  2 siblings, 1 reply; 8+ messages in thread
From: Josef Bacik @ 2026-02-24 16:40 UTC (permalink / raw)
  To: bpf

Add support for specifying multiple file sources in 'bpftool btf dump'
to generate a single C header containing types from vmlinux plus
multiple kernel modules:

  bpftool btf dump file /sys/kernel/btf/mod1 file /sys/kernel/btf/mod2 format c

This is useful for BPF programs that need to access types defined in
kernel modules. Previously this required a separate bpftool invocation
for each module, producing separate headers that could not be combined
due to overlapping vmlinux type definitions.

The implementation collects all file paths, then for the multi-file
case creates an empty split BTF on the vmlinux base and iteratively
merges each module's types into it via btf__add_btf(). The single-file
code path is preserved exactly to avoid any regression risk.

Auto-detection of vmlinux as the base BTF from sysfs paths works as
before. If vmlinux itself appears in the file list it is skipped with
a warning since its types are already provided by the base.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
 .../bpf/bpftool/Documentation/bpftool-btf.rst |  11 +-
 tools/bpf/bpftool/bash-completion/bpftool     |   6 +-
 tools/bpf/bpftool/btf.c                       | 128 ++++++++++++++++--
 3 files changed, 127 insertions(+), 18 deletions(-)

diff --git a/tools/bpf/bpftool/Documentation/bpftool-btf.rst b/tools/bpf/bpftool/Documentation/bpftool-btf.rst
index d47dddc2b4ee..cf75a7fa2d6b 100644
--- a/tools/bpf/bpftool/Documentation/bpftool-btf.rst
+++ b/tools/bpf/bpftool/Documentation/bpftool-btf.rst
@@ -27,7 +27,7 @@ BTF COMMANDS
 | **bpftool** **btf dump** *BTF_SRC* [**format** *FORMAT*] [**root_id** *ROOT_ID*]
 | **bpftool** **btf help**
 |
-| *BTF_SRC* := { **id** *BTF_ID* | **prog** *PROG* | **map** *MAP* [{**key** | **value** | **kv** | **all**}] | **file** *FILE* }
+| *BTF_SRC* := { **id** *BTF_ID* | **prog** *PROG* | **map** *MAP* [{**key** | **value** | **kv** | **all**}] | **file** *FILE* [**file** *FILE*]... }
 | *FORMAT* := { **raw** | **c** [**unsorted**] }
 | *MAP* := { **id** *MAP_ID* | **pinned** *FILE* }
 | *PROG* := { **id** *PROG_ID* | **pinned** *FILE* | **tag** *PROG_TAG* | **name** *PROG_NAME* }
@@ -58,9 +58,12 @@ bpftool btf dump *BTF_SRC* [format *FORMAT*] [root_id *ROOT_ID*]
     When **prog** is provided, it's expected that program has associated BTF
     object with BTF types.
 
-    When specifying *FILE*, an ELF file is expected, containing .BTF section
-    with well-defined BTF binary format data, typically produced by clang or
-    pahole.
+    When specifying *FILE*, an ELF file or a raw BTF file (e.g. from
+    ``/sys/kernel/btf/``) is expected.  Multiple **file** arguments may be
+    given to merge BTF from several kernel modules into a single output.
+    When sysfs paths are used, vmlinux BTF is loaded automatically as the
+    base; if vmlinux itself appears in the file list it is skipped.
+    A base BTF can also be specified explicitly with **-B**.
 
     **format** option can be used to override default (raw) output format. Raw
     (**raw**) or C-syntax (**c**) output formats are supported. With C-style
diff --git a/tools/bpf/bpftool/bash-completion/bpftool b/tools/bpf/bpftool/bash-completion/bpftool
index a28f0cc522e4..babb0d4e9753 100644
--- a/tools/bpf/bpftool/bash-completion/bpftool
+++ b/tools/bpf/bpftool/bash-completion/bpftool
@@ -961,10 +961,14 @@ _bpftool()
                         *)
                             # emit extra options
                             case ${words[3]} in
-                                id|file)
+                                id)
                                     COMPREPLY=( $( compgen -W "root_id" -- "$cur" ) )
                                     _bpftool_once_attr 'format'
                                     ;;
+                                file)
+                                    COMPREPLY=( $( compgen -W "root_id file" -- "$cur" ) )
+                                    _bpftool_once_attr 'format'
+                                    ;;
                                 map|prog)
                                     if [[ ${words[3]} == "map" ]] && [[ $cword == 6 ]]; then
                                         COMPREPLY+=( $( compgen -W "key value kv all" -- "$cur" ) )
diff --git a/tools/bpf/bpftool/btf.c b/tools/bpf/bpftool/btf.c
index 946612029dee..a259a0cfeb3b 100644
--- a/tools/bpf/bpftool/btf.c
+++ b/tools/bpf/bpftool/btf.c
@@ -28,6 +28,7 @@
 #define FASTCALL_DECL_TAG	"bpf_fastcall"
 
 #define MAX_ROOT_IDS		16
+#define MAX_BTF_FILES		64
 
 static const char * const btf_kind_str[NR_BTF_KINDS] = {
 	[BTF_KIND_UNKN]		= "UNKNOWN",
@@ -878,6 +879,49 @@ static bool btf_is_kernel_module(__u32 btf_id)
 	return btf_info.kernel_btf && strncmp(btf_name, "vmlinux", sizeof(btf_name)) != 0;
 }
 
+static struct btf *merge_btf_files(const char **files, int nr_files,
+				   struct btf *vmlinux_base)
+{
+	struct btf *combined, *mod;
+	int ret;
+
+	combined = btf__new_empty_split(vmlinux_base);
+	if (!combined) {
+		p_err("failed to create combined BTF: %s",
+		      strerror(errno));
+		return NULL;
+	}
+
+	for (int j = 0; j < nr_files; j++) {
+		mod = btf__parse_split(files[j], vmlinux_base);
+		if (!mod) {
+			p_err("failed to load BTF from %s: %s",
+			      files[j], strerror(errno));
+			btf__free(combined);
+			return NULL;
+		}
+
+		ret = btf__add_btf(combined, mod);
+		btf__free(mod);
+		if (ret < 0) {
+			p_err("failed to merge BTF from %s: %s",
+			      files[j], strerror(-ret));
+			btf__free(combined);
+			return NULL;
+		}
+	}
+
+	ret = btf__dedup(combined, NULL);
+	if (ret) {
+		p_err("failed to dedup combined BTF: %s",
+		      strerror(-ret));
+		btf__free(combined);
+		return NULL;
+	}
+
+	return combined;
+}
+
 static int do_dump(int argc, char **argv)
 {
 	bool dump_c = false, sort_dump_c = true;
@@ -958,20 +1002,77 @@ static int do_dump(int argc, char **argv)
 		NEXT_ARG();
 	} else if (is_prefix(src, "file")) {
 		const char sysfs_prefix[] = "/sys/kernel/btf/";
+		struct btf *vmlinux_base = base_btf;
+		const char *files[MAX_BTF_FILES];
+		int nr_files = 0;
 
-		if (!base_btf &&
-		    strncmp(*argv, sysfs_prefix, sizeof(sysfs_prefix) - 1) == 0 &&
-		    strcmp(*argv, sysfs_vmlinux) != 0)
-			base = get_vmlinux_btf_from_sysfs();
-
-		btf = btf__parse_split(*argv, base ?: base_btf);
-		if (!btf) {
-			err = -errno;
-			p_err("failed to load BTF from %s: %s",
-			      *argv, strerror(errno));
-			goto done;
-		}
+		/* First grab our argument, filtering out the sysfs_vmlinux. */
+		if (strcmp(*argv, sysfs_vmlinux) != 0)
+			files[nr_files++] = *argv;
 		NEXT_ARG();
+
+		while (argc && strcmp(*argv, "file") == 0) {
+			NEXT_ARG();
+			if (!REQ_ARGS(1)) {
+				err = -EINVAL;
+				goto done;
+			}
+			/* Filter out any sysfs vmlinux entries. */
+			if (strcmp(*argv, sysfs_vmlinux) == 0) {
+				NEXT_ARG();
+				continue;
+			}
+			if (nr_files >= MAX_BTF_FILES) {
+				p_err("too many BTF files (max %d)",
+				      MAX_BTF_FILES);
+				err = -E2BIG;
+				goto done;
+			}
+			files[nr_files++] = *argv;
+			NEXT_ARG();
+		}
+
+		/* Auto-detect vmlinux base if any file is from sysfs */
+		if (!vmlinux_base) {
+			for (int j = 0; j < nr_files; j++) {
+				if (strncmp(files[j], sysfs_prefix,
+					    sizeof(sysfs_prefix) - 1) == 0) {
+					base = get_vmlinux_btf_from_sysfs();
+					vmlinux_base = base;
+					break;
+				}
+			}
+		}
+
+		/* All files were the sysfs_vmlinux, handle it like we used to */
+		if (nr_files == 0) {
+			nr_files = 1;
+			files[0] = sysfs_vmlinux;
+		}
+
+		if (nr_files == 1) {
+			btf = btf__parse_split(files[0],
+					       base ?: base_btf);
+			if (!btf) {
+				err = -errno;
+				p_err("failed to load BTF from %s: %s",
+				      files[0], strerror(errno));
+				goto done;
+			}
+		} else {
+			if (!vmlinux_base) {
+				p_err("base BTF is required when merging multiple BTF files; use -B/--base-btf or use sysfs paths");
+				err = -EINVAL;
+				goto done;
+			}
+
+			btf = merge_btf_files(files, nr_files,
+					      vmlinux_base);
+			if (!btf) {
+				err = -errno;
+				goto done;
+			}
+		}
 	} else {
 		err = -1;
 		p_err("unrecognized BTF source specifier: '%s'", src);
@@ -1445,7 +1546,8 @@ static int do_help(int argc, char **argv)
 		"       %1$s %2$s dump BTF_SRC [format FORMAT] [root_id ROOT_ID]\n"
 		"       %1$s %2$s help\n"
 		"\n"
-		"       BTF_SRC := { id BTF_ID | prog PROG | map MAP [{key | value | kv | all}] | file FILE }\n"
+		"       BTF_SRC := { id BTF_ID | prog PROG | map MAP [{key | value | kv | all}] |\n"
+		"                    file FILE [file FILE]... }\n"
 		"       FORMAT  := { raw | c [unsorted] }\n"
 		"       " HELP_SPEC_MAP "\n"
 		"       " HELP_SPEC_PROGRAM "\n"
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v5 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources
  2026-02-24 16:40 [PATCH v5 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
  2026-02-24 16:40 ` [PATCH v5 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
  2026-02-24 16:40 ` [PATCH v5 2/3] bpftool: support merging multiple module BTFs in btf dump Josef Bacik
@ 2026-02-24 16:40 ` Josef Bacik
  2026-02-24 17:05   ` Alan Maguire
  2 siblings, 1 reply; 8+ messages in thread
From: Josef Bacik @ 2026-02-24 16:40 UTC (permalink / raw)
  To: bpf

Add a test that verifies btf__add_btf() correctly handles merging
multiple split BTF objects that share the same base BTF. The test
creates two sibling split BTFs on a common base, merges them into
a combined split BTF, and validates that base type references are
preserved while split type references are properly remapped.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
 .../selftests/bpf/prog_tests/btf_write.c      | 111 ++++++++++++++++++
 1 file changed, 111 insertions(+)

diff --git a/tools/testing/selftests/bpf/prog_tests/btf_write.c b/tools/testing/selftests/bpf/prog_tests/btf_write.c
index 6e36de1302fc..5c84723cf254 100644
--- a/tools/testing/selftests/bpf/prog_tests/btf_write.c
+++ b/tools/testing/selftests/bpf/prog_tests/btf_write.c
@@ -497,10 +497,121 @@ static void test_btf_add_btf()
 	btf__free(btf2);
 }
 
+static void test_btf_add_btf_split()
+{
+	struct btf *base = NULL, *split1 = NULL, *split2 = NULL;
+	struct btf *combined = NULL;
+	int id, err;
+
+	/* Create a base BTF with an INT and a PTR to it */
+	base = btf__new_empty();
+	if (!ASSERT_OK_PTR(base, "base"))
+		return;
+
+	id = btf__add_int(base, "int", 4, BTF_INT_SIGNED);
+	ASSERT_EQ(id, 1, "base_int_id");
+	id = btf__add_ptr(base, 1);
+	ASSERT_EQ(id, 2, "base_ptr_id");
+
+	/* base has 2 types, type IDs 1..2 */
+	ASSERT_EQ(btf__type_cnt(base), 3, "base_type_cnt");
+
+	/* Create split1 on base: a STRUCT referencing base's int (ID 1) */
+	split1 = btf__new_empty_split(base);
+	if (!ASSERT_OK_PTR(split1, "split1"))
+		goto cleanup;
+
+	id = btf__add_struct(split1, "s1", 4);
+	/* split types start at base_type_cnt = 3 */
+	ASSERT_EQ(id, 3, "split1_struct_id");
+	btf__add_field(split1, "x", 1, 0, 0); /* refers to base int */
+
+	id = btf__add_ptr(split1, 3);
+	ASSERT_EQ(id, 4, "split1_ptr_id"); /* ptr to the struct (split self-ref) */
+
+	/* Add a typedef "int_alias" -> base int in split1, which will be
+	 * duplicated in split2 to test that btf__dedup() merges them.
+	 */
+	id = btf__add_typedef(split1, "int_alias", 1);
+	ASSERT_EQ(id, 5, "split1_typedef_id");
+
+	/* Create split2 on base: a TYPEDEF referencing base's ptr (ID 2) */
+	split2 = btf__new_empty_split(base);
+	if (!ASSERT_OK_PTR(split2, "split2"))
+		goto cleanup;
+
+	id = btf__add_typedef(split2, "int_ptr", 2); /* refers to base ptr */
+	ASSERT_EQ(id, 3, "split2_typedef_id");
+
+	id = btf__add_struct(split2, "s2", 8);
+	ASSERT_EQ(id, 4, "split2_struct_id");
+	btf__add_field(split2, "p", 3, 0, 0); /* refers to split2's own typedef */
+
+	/* Same "int_alias" typedef as split1 - should be deduped away */
+	id = btf__add_typedef(split2, "int_alias", 1);
+	ASSERT_EQ(id, 5, "split2_dup_typedef_id");
+
+	/* Create combined split BTF on same base and merge both */
+	combined = btf__new_empty_split(base);
+	if (!ASSERT_OK_PTR(combined, "combined"))
+		goto cleanup;
+
+	/* Merge split1: its types (3,4,5) should land at IDs 3,4,5 */
+	id = btf__add_btf(combined, split1);
+	if (!ASSERT_GE(id, 0, "add_split1"))
+		goto cleanup;
+	ASSERT_EQ(id, 3, "split1_first_id");
+
+	/* Merge split2: its types (3,4,5) should be remapped to 6,7,8 */
+	id = btf__add_btf(combined, split2);
+	if (!ASSERT_GE(id, 0, "add_split2"))
+		goto cleanup;
+	ASSERT_EQ(id, 6, "split2_first_id");
+
+	/* Before dedup: base (2) + split1 (3) + split2 (3) = 8 types + void */
+	ASSERT_EQ(btf__type_cnt(combined), 9, "pre_dedup_type_cnt");
+
+	VALIDATE_RAW_BTF(
+		combined,
+		/* base types (IDs 1-2) */
+		"[1] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED",
+		"[2] PTR '(anon)' type_id=1",
+
+		/* split1 types (IDs 3-5): base refs unchanged */
+		"[3] STRUCT 's1' size=4 vlen=1\n"
+		"\t'x' type_id=1 bits_offset=0",      /* refers to base int=1 */
+		"[4] PTR '(anon)' type_id=3",          /* refers to split1's struct=3 */
+		"[5] TYPEDEF 'int_alias' type_id=1",   /* refers to base int=1 */
+
+		/* split2 types (IDs 6-8): remapped from 3,4,5 to 6,7,8 */
+		"[6] TYPEDEF 'int_ptr' type_id=2",     /* base ptr=2, unchanged */
+		"[7] STRUCT 's2' size=8 vlen=1\n"
+		"\t'p' type_id=6 bits_offset=0",       /* split2 typedef: 3->6 */
+		"[8] TYPEDEF 'int_alias' type_id=1");   /* dup of [5] */
+
+	/* Dedup to mirror the bpftool merge flow; should remove the
+	 * duplicate "int_alias" typedef.
+	 */
+	err = btf__dedup(combined, NULL);
+	if (!ASSERT_OK(err, "dedup"))
+		goto cleanup;
+
+	/* After dedup: one int_alias removed, so 7 types + void */
+	ASSERT_EQ(btf__type_cnt(combined), 8, "dedup_type_cnt");
+
+cleanup:
+	btf__free(combined);
+	btf__free(split2);
+	btf__free(split1);
+	btf__free(base);
+}
+
 void test_btf_write()
 {
 	if (test__start_subtest("btf_add"))
 		test_btf_add();
 	if (test__start_subtest("btf_add_btf"))
 		test_btf_add_btf();
+	if (test__start_subtest("btf_add_btf_split"))
+		test_btf_add_btf_split();
 }
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH v5 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources
  2026-02-24 16:40 ` [PATCH v5 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources Josef Bacik
@ 2026-02-24 17:05   ` Alan Maguire
  0 siblings, 0 replies; 8+ messages in thread
From: Alan Maguire @ 2026-02-24 17:05 UTC (permalink / raw)
  To: Josef Bacik, bpf

On 24/02/2026 16:40, Josef Bacik wrote:
> Add a test that verifies btf__add_btf() correctly handles merging
> multiple split BTF objects that share the same base BTF. The test
> creates two sibling split BTFs on a common base, merges them into
> a combined split BTF, and validates that base type references are
> preserved while split type references are properly remapped.
> 
> Assisted-by: Claude:claude-opus-4-6
> Signed-off-by: Josef Bacik <josef@toxicpanda.com>

Reviewed-by: Alan Maguire <alan.maguire@oracle.com>

> ---
>  .../selftests/bpf/prog_tests/btf_write.c      | 111 ++++++++++++++++++
>  1 file changed, 111 insertions(+)
> 
> diff --git a/tools/testing/selftests/bpf/prog_tests/btf_write.c b/tools/testing/selftests/bpf/prog_tests/btf_write.c
> index 6e36de1302fc..5c84723cf254 100644
> --- a/tools/testing/selftests/bpf/prog_tests/btf_write.c
> +++ b/tools/testing/selftests/bpf/prog_tests/btf_write.c
> @@ -497,10 +497,121 @@ static void test_btf_add_btf()
>  	btf__free(btf2);
>  }
>  
> +static void test_btf_add_btf_split()
> +{
> +	struct btf *base = NULL, *split1 = NULL, *split2 = NULL;
> +	struct btf *combined = NULL;
> +	int id, err;
> +
> +	/* Create a base BTF with an INT and a PTR to it */
> +	base = btf__new_empty();
> +	if (!ASSERT_OK_PTR(base, "base"))
> +		return;
> +
> +	id = btf__add_int(base, "int", 4, BTF_INT_SIGNED);
> +	ASSERT_EQ(id, 1, "base_int_id");
> +	id = btf__add_ptr(base, 1);
> +	ASSERT_EQ(id, 2, "base_ptr_id");
> +
> +	/* base has 2 types, type IDs 1..2 */
> +	ASSERT_EQ(btf__type_cnt(base), 3, "base_type_cnt");
> +
> +	/* Create split1 on base: a STRUCT referencing base's int (ID 1) */
> +	split1 = btf__new_empty_split(base);
> +	if (!ASSERT_OK_PTR(split1, "split1"))
> +		goto cleanup;
> +
> +	id = btf__add_struct(split1, "s1", 4);
> +	/* split types start at base_type_cnt = 3 */
> +	ASSERT_EQ(id, 3, "split1_struct_id");
> +	btf__add_field(split1, "x", 1, 0, 0); /* refers to base int */
> +
> +	id = btf__add_ptr(split1, 3);
> +	ASSERT_EQ(id, 4, "split1_ptr_id"); /* ptr to the struct (split self-ref) */
> +
> +	/* Add a typedef "int_alias" -> base int in split1, which will be
> +	 * duplicated in split2 to test that btf__dedup() merges them.
> +	 */
> +	id = btf__add_typedef(split1, "int_alias", 1);
> +	ASSERT_EQ(id, 5, "split1_typedef_id");
> +
> +	/* Create split2 on base: a TYPEDEF referencing base's ptr (ID 2) */
> +	split2 = btf__new_empty_split(base);
> +	if (!ASSERT_OK_PTR(split2, "split2"))
> +		goto cleanup;
> +
> +	id = btf__add_typedef(split2, "int_ptr", 2); /* refers to base ptr */
> +	ASSERT_EQ(id, 3, "split2_typedef_id");
> +
> +	id = btf__add_struct(split2, "s2", 8);
> +	ASSERT_EQ(id, 4, "split2_struct_id");
> +	btf__add_field(split2, "p", 3, 0, 0); /* refers to split2's own typedef */
> +
> +	/* Same "int_alias" typedef as split1 - should be deduped away */
> +	id = btf__add_typedef(split2, "int_alias", 1);
> +	ASSERT_EQ(id, 5, "split2_dup_typedef_id");
> +
> +	/* Create combined split BTF on same base and merge both */
> +	combined = btf__new_empty_split(base);
> +	if (!ASSERT_OK_PTR(combined, "combined"))
> +		goto cleanup;
> +
> +	/* Merge split1: its types (3,4,5) should land at IDs 3,4,5 */
> +	id = btf__add_btf(combined, split1);
> +	if (!ASSERT_GE(id, 0, "add_split1"))
> +		goto cleanup;
> +	ASSERT_EQ(id, 3, "split1_first_id");
> +
> +	/* Merge split2: its types (3,4,5) should be remapped to 6,7,8 */
> +	id = btf__add_btf(combined, split2);
> +	if (!ASSERT_GE(id, 0, "add_split2"))
> +		goto cleanup;
> +	ASSERT_EQ(id, 6, "split2_first_id");
> +
> +	/* Before dedup: base (2) + split1 (3) + split2 (3) = 8 types + void */
> +	ASSERT_EQ(btf__type_cnt(combined), 9, "pre_dedup_type_cnt");
> +
> +	VALIDATE_RAW_BTF(
> +		combined,
> +		/* base types (IDs 1-2) */
> +		"[1] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED",
> +		"[2] PTR '(anon)' type_id=1",
> +
> +		/* split1 types (IDs 3-5): base refs unchanged */
> +		"[3] STRUCT 's1' size=4 vlen=1\n"
> +		"\t'x' type_id=1 bits_offset=0",      /* refers to base int=1 */
> +		"[4] PTR '(anon)' type_id=3",          /* refers to split1's struct=3 */
> +		"[5] TYPEDEF 'int_alias' type_id=1",   /* refers to base int=1 */
> +
> +		/* split2 types (IDs 6-8): remapped from 3,4,5 to 6,7,8 */
> +		"[6] TYPEDEF 'int_ptr' type_id=2",     /* base ptr=2, unchanged */
> +		"[7] STRUCT 's2' size=8 vlen=1\n"
> +		"\t'p' type_id=6 bits_offset=0",       /* split2 typedef: 3->6 */
> +		"[8] TYPEDEF 'int_alias' type_id=1");   /* dup of [5] */
> +
> +	/* Dedup to mirror the bpftool merge flow; should remove the
> +	 * duplicate "int_alias" typedef.
> +	 */
> +	err = btf__dedup(combined, NULL);
> +	if (!ASSERT_OK(err, "dedup"))
> +		goto cleanup;
> +
> +	/* After dedup: one int_alias removed, so 7 types + void */
> +	ASSERT_EQ(btf__type_cnt(combined), 8, "dedup_type_cnt");
> +
> +cleanup:
> +	btf__free(combined);
> +	btf__free(split2);
> +	btf__free(split1);
> +	btf__free(base);
> +}
> +
>  void test_btf_write()
>  {
>  	if (test__start_subtest("btf_add"))
>  		test_btf_add();
>  	if (test__start_subtest("btf_add_btf"))
>  		test_btf_add_btf();
> +	if (test__start_subtest("btf_add_btf_split"))
> +		test_btf_add_btf_split();
>  }


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v5 2/3] bpftool: support merging multiple module BTFs in btf dump
  2026-02-24 16:40 ` [PATCH v5 2/3] bpftool: support merging multiple module BTFs in btf dump Josef Bacik
@ 2026-02-24 17:18   ` bot+bpf-ci
  2026-02-26 13:47     ` Quentin Monnet
  0 siblings, 1 reply; 8+ messages in thread
From: bot+bpf-ci @ 2026-02-24 17:18 UTC (permalink / raw)
  To: josef, bpf
  Cc: ast, andrii, daniel, martin.lau, eddyz87, yonghong.song, clm,
	ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 2339 bytes --]

> Auto-detection of vmlinux as the base BTF from sysfs paths works as
> before. If vmlinux itself appears in the file list it is skipped with
> a warning since its types are already provided by the base.

The commit message says vmlinux is "skipped with a warning" but the
code filters vmlinux entries silently.  In do_dump(), the first file
is filtered with:

    if (strcmp(*argv, sysfs_vmlinux) != 0)
        files[nr_files++] = *argv;

and subsequent files with:

    if (strcmp(*argv, sysfs_vmlinux) == 0) {
        NEXT_ARG();
        continue;
    }

Neither path emits a p_info() or any other diagnostic.  The v4 version
of this patch had a p_info("skipping %s (already loaded as base)")
call which was dropped during restructuring for v5.  Should the warning
be restored, or should the commit message be updated to match the
actual behavior?

> diff --git a/tools/bpf/bpftool/btf.c b/tools/bpf/bpftool/btf.c
> --- a/tools/bpf/bpftool/btf.c
> +++ b/tools/bpf/bpftool/btf.c

[ ... ]

> +		while (argc && strcmp(*argv, "file") == 0) {
> +			NEXT_ARG();
> +			if (!REQ_ARGS(1)) {
> +				err = -EINVAL;
> +				goto done;
> +			}

Quentin Monnet asked in v4 review why strcmp() is used here instead of
is_prefix(), which is the standard pattern throughout bpftool for
keyword matching:

https://lore.kernel.org/bpf/4ccb0ad0-3e50-4548-b8d6-e2d7e93b3dad@qmon.net/

The initial "file" keyword is matched via is_prefix(src, "file") at
the top of do_dump(), so abbreviated forms work for the first file
argument but not for subsequent ones.  Was this intentional?

> +			/* Filter out any sysfs vmlinux entries. */
> +			if (strcmp(*argv, sysfs_vmlinux) == 0) {
> +				NEXT_ARG();
> +				continue;
> +			}

[ ... ]


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/22361062191

AI-authorship-score: high
AI-authorship-explanation: The commit explicitly declares AI assistance via the Assisted-by: Claude:claude-opus-4-6 tag.
issues-found: 2
issue-severity-score: low
issue-severity-explanation: Commit message inaccuracy about a missing warning and an unaddressed maintainer review comment about keyword matching convention; neither causes functional problems.

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v5 2/3] bpftool: support merging multiple module BTFs in btf dump
  2026-02-24 17:18   ` bot+bpf-ci
@ 2026-02-26 13:47     ` Quentin Monnet
  2026-03-03 16:42       ` Alexei Starovoitov
  0 siblings, 1 reply; 8+ messages in thread
From: Quentin Monnet @ 2026-02-26 13:47 UTC (permalink / raw)
  To: josef, bpf
  Cc: ast, andrii, daniel, martin.lau, eddyz87, yonghong.song, clm,
	ihor.solodrai

2026-02-24 17:18 UTC+0000 ~ bot+bpf-ci@kernel.org
>> Auto-detection of vmlinux as the base BTF from sysfs paths works as
>> before. If vmlinux itself appears in the file list it is skipped with
>> a warning since its types are already provided by the base.
> 
> The commit message says vmlinux is "skipped with a warning" but the
> code filters vmlinux entries silently.  In do_dump(), the first file
> is filtered with:
> 
>     if (strcmp(*argv, sysfs_vmlinux) != 0)
>         files[nr_files++] = *argv;
> 
> and subsequent files with:
> 
>     if (strcmp(*argv, sysfs_vmlinux) == 0) {
>         NEXT_ARG();
>         continue;
>     }
> 
> Neither path emits a p_info() or any other diagnostic.  The v4 version
> of this patch had a p_info("skipping %s (already loaded as base)")
> call which was dropped during restructuring for v5.  Should the warning
> be restored, or should the commit message be updated to match the
> actual behavior?
> 
>> diff --git a/tools/bpf/bpftool/btf.c b/tools/bpf/bpftool/btf.c
>> --- a/tools/bpf/bpftool/btf.c
>> +++ b/tools/bpf/bpftool/btf.c
> 
> [ ... ]
> 
>> +		while (argc && strcmp(*argv, "file") == 0) {
>> +			NEXT_ARG();
>> +			if (!REQ_ARGS(1)) {
>> +				err = -EINVAL;
>> +				goto done;
>> +			}
> 
> Quentin Monnet asked in v4 review why strcmp() is used here instead of
> is_prefix(), which is the standard pattern throughout bpftool for
> keyword matching:


And the question still stands :)
The rest of the bpftool patch looks good to me, thank you for the changes.


> https://lore.kernel.org/bpf/4ccb0ad0-3e50-4548-b8d6-e2d7e93b3dad@qmon.net/
> 
> The initial "file" keyword is matched via is_prefix(src, "file") at
> the top of do_dump(), so abbreviated forms work for the first file
> argument but not for subsequent ones.  Was this intentional?
> 
>> +			/* Filter out any sysfs vmlinux entries. */
>> +			if (strcmp(*argv, sysfs_vmlinux) == 0) {
>> +				NEXT_ARG();
>> +				continue;
>> +			}
> 
> [ ... ]

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v5 2/3] bpftool: support merging multiple module BTFs in btf dump
  2026-02-26 13:47     ` Quentin Monnet
@ 2026-03-03 16:42       ` Alexei Starovoitov
  0 siblings, 0 replies; 8+ messages in thread
From: Alexei Starovoitov @ 2026-03-03 16:42 UTC (permalink / raw)
  To: Quentin Monnet
  Cc: Josef Bacik, bpf, Alexei Starovoitov, Andrii Nakryiko,
	Daniel Borkmann, Martin KaFai Lau, Eduard, Yonghong Song,
	Chris Mason, Ihor Solodrai

On Thu, Feb 26, 2026 at 5:47 AM Quentin Monnet <qmo@qmon.net> wrote:
>
> 2026-02-24 17:18 UTC+0000 ~ bot+bpf-ci@kernel.org
> >> Auto-detection of vmlinux as the base BTF from sysfs paths works as
> >> before. If vmlinux itself appears in the file list it is skipped with
> >> a warning since its types are already provided by the base.
> >
> > The commit message says vmlinux is "skipped with a warning" but the
> > code filters vmlinux entries silently.  In do_dump(), the first file
> > is filtered with:
> >
> >     if (strcmp(*argv, sysfs_vmlinux) != 0)
> >         files[nr_files++] = *argv;
> >
> > and subsequent files with:
> >
> >     if (strcmp(*argv, sysfs_vmlinux) == 0) {
> >         NEXT_ARG();
> >         continue;
> >     }
> >
> > Neither path emits a p_info() or any other diagnostic.  The v4 version
> > of this patch had a p_info("skipping %s (already loaded as base)")
> > call which was dropped during restructuring for v5.  Should the warning
> > be restored, or should the commit message be updated to match the
> > actual behavior?
> >
> >> diff --git a/tools/bpf/bpftool/btf.c b/tools/bpf/bpftool/btf.c
> >> --- a/tools/bpf/bpftool/btf.c
> >> +++ b/tools/bpf/bpftool/btf.c
> >
> > [ ... ]
> >
> >> +            while (argc && strcmp(*argv, "file") == 0) {
> >> +                    NEXT_ARG();
> >> +                    if (!REQ_ARGS(1)) {
> >> +                            err = -EINVAL;
> >> +                            goto done;
> >> +                    }
> >
> > Quentin Monnet asked in v4 review why strcmp() is used here instead of
> > is_prefix(), which is the standard pattern throughout bpftool for
> > keyword matching:
>
>
> And the question still stands :)
> The rest of the bpftool patch looks good to me, thank you for the changes.

I was thinking to patch it up while applying, but I'm not sure
whether just that strcmp needs to change or all of them below as well.
Please fix and respin.

pw-bot: cr

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-03-03 16:42 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-24 16:40 [PATCH v5 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
2026-02-24 16:40 ` [PATCH v5 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
2026-02-24 16:40 ` [PATCH v5 2/3] bpftool: support merging multiple module BTFs in btf dump Josef Bacik
2026-02-24 17:18   ` bot+bpf-ci
2026-02-26 13:47     ` Quentin Monnet
2026-03-03 16:42       ` Alexei Starovoitov
2026-02-24 16:40 ` [PATCH v5 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources Josef Bacik
2026-02-24 17:05   ` Alan Maguire

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox