public inbox for bpf@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH v6 0/3] libbpf/bpftool: support merging split BTFs
@ 2026-03-04 20:56 Josef Bacik
  2026-03-04 20:56 ` [PATCH v6 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
                   ` (3 more replies)
  0 siblings, 4 replies; 9+ messages in thread
From: Josef Bacik @ 2026-03-04 20:56 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/
v5: https://lore.kernel.org/bpf/cover.1771950922.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.
v5->v6:
- Fixed the missed is_prefix comment.
- Fixed the removed warning about skipping vmlinux.

--- 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                       | 132 ++++++++++++++++--
 tools/lib/bpf/btf.c                           |  37 +++--
 .../selftests/bpf/prog_tests/btf_write.c      | 111 +++++++++++++++
 5 files changed, 272 insertions(+), 25 deletions(-)

-- 
2.53.0


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

* [PATCH v6 1/3] libbpf: support appending split BTF in btf__add_btf()
  2026-03-04 20:56 [PATCH v6 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
@ 2026-03-04 20:56 ` Josef Bacik
  2026-03-04 22:12   ` Alan Maguire
  2026-03-05 23:06   ` Andrii Nakryiko
  2026-03-04 20:56 ` [PATCH v6 2/3] bpftool: support merging multiple module BTFs in btf dump Josef Bacik
                   ` (2 subsequent siblings)
  3 siblings, 2 replies; 9+ messages in thread
From: Josef Bacik @ 2026-03-04 20:56 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] 9+ messages in thread

* [PATCH v6 2/3] bpftool: support merging multiple module BTFs in btf dump
  2026-03-04 20:56 [PATCH v6 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
  2026-03-04 20:56 ` [PATCH v6 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
@ 2026-03-04 20:56 ` Josef Bacik
  2026-03-04 22:19   ` Alan Maguire
  2026-03-05 23:06   ` Andrii Nakryiko
  2026-03-04 20:56 ` [PATCH v6 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources Josef Bacik
  2026-03-05 23:10 ` [PATCH v6 0/3] libbpf/bpftool: support merging split BTFs patchwork-bot+netdevbpf
  3 siblings, 2 replies; 9+ messages in thread
From: Josef Bacik @ 2026-03-04 20:56 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                       | 132 ++++++++++++++++--
 3 files changed, 132 insertions(+), 17 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..c44e86a64760 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,83 @@ 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;
+		} else {
+			p_info("skipping %s (will be loaded as base)",
+			       *argv);
 		}
 		NEXT_ARG();
+
+		while (argc && is_prefix(*argv, "file")) {
+			NEXT_ARG();
+			if (!REQ_ARGS(1)) {
+				err = -EINVAL;
+				goto done;
+			}
+			/* Filter out any sysfs vmlinux entries. */
+			if (strcmp(*argv, sysfs_vmlinux) == 0) {
+				p_info("skipping %s (will be loaded as base)",
+				       *argv);
+				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 +1552,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] 9+ messages in thread

* [PATCH v6 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources
  2026-03-04 20:56 [PATCH v6 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
  2026-03-04 20:56 ` [PATCH v6 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
  2026-03-04 20:56 ` [PATCH v6 2/3] bpftool: support merging multiple module BTFs in btf dump Josef Bacik
@ 2026-03-04 20:56 ` Josef Bacik
  2026-03-05 23:10 ` [PATCH v6 0/3] libbpf/bpftool: support merging split BTFs patchwork-bot+netdevbpf
  3 siblings, 0 replies; 9+ messages in thread
From: Josef Bacik @ 2026-03-04 20:56 UTC (permalink / raw)
  To: bpf; +Cc: Alan Maguire

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();
 }
-- 
2.53.0


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

* Re: [PATCH v6 1/3] libbpf: support appending split BTF in btf__add_btf()
  2026-03-04 20:56 ` [PATCH v6 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
@ 2026-03-04 22:12   ` Alan Maguire
  2026-03-05 23:06   ` Andrii Nakryiko
  1 sibling, 0 replies; 9+ messages in thread
From: Alan Maguire @ 2026-03-04 22:12 UTC (permalink / raw)
  To: Josef Bacik, bpf

On 04/03/2026 20:56, Josef Bacik wrote:
> 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>

a few optional nits for readability below, but

Reviewed-by: Alan Maguire <alan.maguire@oracle.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;

would maybe be a bit more readable (if a little less concise) as

	src_start_id = 1;
	cnt = btf__type_cnt(src_btf);
	if (src_btf->base_btf) {
		src_start_id = btf__type_cnt(src_btf->base_btf);
		cnt -= btf__type_cnt(src_btf->base_btf);
	}

(might be an off-by-one error lurking there but I _think_ it's right).
I _think_ we'd rather use the type counting functions where possible
as opposed to dereferencing 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;

not your change really but could we compute this offset addition once as
part of the above prior to the loop, i.e.

	src_start_id = 1;
	cnt = btf__type_cnt(src_btf);
	new_id_off = 0;
	if (src_btf->base_btf) {
		src_start_id = btf__type_cnt(src_btf->base_btf);
		cnt -= btf__type_cnt(src_btf->base_btf);
		new_id_off = btf__type_cnt(src_btf) - 1;
	}

...and then use it unconditionally as

			*type_id += new_id_off;

?

>  		}
>  
>  		/* go to next type data and type offset index entry */


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

* Re: [PATCH v6 2/3] bpftool: support merging multiple module BTFs in btf dump
  2026-03-04 20:56 ` [PATCH v6 2/3] bpftool: support merging multiple module BTFs in btf dump Josef Bacik
@ 2026-03-04 22:19   ` Alan Maguire
  2026-03-05 23:06   ` Andrii Nakryiko
  1 sibling, 0 replies; 9+ messages in thread
From: Alan Maguire @ 2026-03-04 22:19 UTC (permalink / raw)
  To: Josef Bacik, bpf

On 04/03/2026 20:56, Josef Bacik wrote:
> 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>

again minor nits, but

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

this is a really useful feature, thanks for working on it!

> ---
>  .../bpf/bpftool/Documentation/bpftool-btf.rst |  11 +-
>  tools/bpf/bpftool/bash-completion/bpftool     |   6 +-
>  tools/bpf/bpftool/btf.c                       | 132 ++++++++++++++++--
>  3 files changed, 132 insertions(+), 17 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..c44e86a64760 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)

here - and below - the naming implies that base BTF has to be vmlinux. While there
is special handling for sysfs paths to use vmlinux as base, it would be good to
just use the more neutral base/base_btf as name I think.


> +{
> +	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,83 @@ 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;
> +		} else {
> +			p_info("skipping %s (will be loaded as base)",
> +			       *argv);
>  		}
>  		NEXT_ARG();
> +
> +		while (argc && is_prefix(*argv, "file")) {
> +			NEXT_ARG();
> +			if (!REQ_ARGS(1)) {
> +				err = -EINVAL;
> +				goto done;
> +			}
> +			/* Filter out any sysfs vmlinux entries. */
> +			if (strcmp(*argv, sysfs_vmlinux) == 0) {
> +				p_info("skipping %s (will be loaded as base)",
> +				       *argv);
> +				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 +1552,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"


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

* Re: [PATCH v6 2/3] bpftool: support merging multiple module BTFs in btf dump
  2026-03-04 20:56 ` [PATCH v6 2/3] bpftool: support merging multiple module BTFs in btf dump Josef Bacik
  2026-03-04 22:19   ` Alan Maguire
@ 2026-03-05 23:06   ` Andrii Nakryiko
  1 sibling, 0 replies; 9+ messages in thread
From: Andrii Nakryiko @ 2026-03-05 23:06 UTC (permalink / raw)
  To: Josef Bacik; +Cc: bpf

On Wed, Mar 4, 2026 at 12:57 PM Josef Bacik <josef@toxicpanda.com> wrote:
>
> 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                       | 132 ++++++++++++++++--
>  3 files changed, 132 insertions(+), 17 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..c44e86a64760 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));

this fits even under 80 characters...

> +                       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));

this is under 100

> +                       btf__free(combined);
> +                       return NULL;
> +               }
> +       }
> +
> +       ret = btf__dedup(combined, NULL);
> +       if (ret) {
> +               p_err("failed to dedup combined BTF: %s",
> +                     strerror(-ret));

ditto

> +               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,83 @@ 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;
> +               } else {
> +                       p_info("skipping %s (will be loaded as base)",
> +                              *argv);

I mean... why is your Claude so frugal with per-line characters?

(fixed all this and a bunch more below)

pushed to bpf-next

>                 }
>                 NEXT_ARG();
> +
> +               while (argc && is_prefix(*argv, "file")) {
> +                       NEXT_ARG();
> +                       if (!REQ_ARGS(1)) {
> +                               err = -EINVAL;
> +                               goto done;
> +                       }
> +                       /* Filter out any sysfs vmlinux entries. */
> +                       if (strcmp(*argv, sysfs_vmlinux) == 0) {
> +                               p_info("skipping %s (will be loaded as base)",
> +                                      *argv);
> +                               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 +1552,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	[flat|nested] 9+ messages in thread

* Re: [PATCH v6 1/3] libbpf: support appending split BTF in btf__add_btf()
  2026-03-04 20:56 ` [PATCH v6 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
  2026-03-04 22:12   ` Alan Maguire
@ 2026-03-05 23:06   ` Andrii Nakryiko
  1 sibling, 0 replies; 9+ messages in thread
From: Andrii Nakryiko @ 2026-03-05 23:06 UTC (permalink / raw)
  To: Josef Bacik; +Cc: bpf

On Wed, Mar 4, 2026 at 12:57 PM Josef Bacik <josef@toxicpanda.com> wrote:
>
> 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)

only Claude could have formatted this in such a way, please ask it to
be more sensible with formatting next time (and it's still on human to
review Claude-generated code and make sure it follows the style,
besides correctness)

> +               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.
> +                        */

humanspeak: "don't remap strings from shared base BTF"

> +                       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.
>                          */

humanspeak: "don't remap types from shared base BTF"


updated while applying

> -                       *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	[flat|nested] 9+ messages in thread

* Re: [PATCH v6 0/3] libbpf/bpftool: support merging split BTFs
  2026-03-04 20:56 [PATCH v6 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
                   ` (2 preceding siblings ...)
  2026-03-04 20:56 ` [PATCH v6 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources Josef Bacik
@ 2026-03-05 23:10 ` patchwork-bot+netdevbpf
  3 siblings, 0 replies; 9+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-03-05 23:10 UTC (permalink / raw)
  To: Josef Bacik; +Cc: bpf

Hello:

This series was applied to bpf/bpf-next.git (master)
by Andrii Nakryiko <andrii@kernel.org>:

On Wed,  4 Mar 2026 15:56:49 -0500 you wrote:
> 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/
> v5: https://lore.kernel.org/bpf/cover.1771950922.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.
> v5->v6:
> - Fixed the missed is_prefix comment.
> - Fixed the removed warning about skipping vmlinux.
> 
> [...]

Here is the summary with links:
  - [v6,1/3] libbpf: support appending split BTF in btf__add_btf()
    (no matching commit)
  - [v6,2/3] bpftool: support merging multiple module BTFs in btf dump
    (no matching commit)
  - [v6,3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources
    https://git.kernel.org/bpf/bpf-next/c/fefeeec61235

You are awesome, thank you!
-- 
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html



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

end of thread, other threads:[~2026-03-05 23:10 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-04 20:56 [PATCH v6 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
2026-03-04 20:56 ` [PATCH v6 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
2026-03-04 22:12   ` Alan Maguire
2026-03-05 23:06   ` Andrii Nakryiko
2026-03-04 20:56 ` [PATCH v6 2/3] bpftool: support merging multiple module BTFs in btf dump Josef Bacik
2026-03-04 22:19   ` Alan Maguire
2026-03-05 23:06   ` Andrii Nakryiko
2026-03-04 20:56 ` [PATCH v6 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources Josef Bacik
2026-03-05 23:10 ` [PATCH v6 0/3] libbpf/bpftool: support merging split BTFs patchwork-bot+netdevbpf

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