* [PATCH 1/3] libbpf: support appending split BTF in btf__add_btf()
2026-02-20 16:48 [PATCH 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
@ 2026-02-20 16:48 ` Josef Bacik
2026-02-20 16:48 ` [PATCH 2/3] bpftool: support merging multiple module BTFs in btf dump Josef Bacik
2026-02-20 16:48 ` [PATCH 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources Josef Bacik
2 siblings, 0 replies; 4+ messages in thread
From: Josef Bacik @ 2026-02-20 16:48 UTC (permalink / raw)
To: bpf; +Cc: Claude Opus 4.6
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.
- 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.
- Pre-emptively calling btf_ensure_modifiable() on the destination's
base BTF to prevent a use-after-free: btf_rewrite_str() resolves
strings via btf__str_by_offset(src) which may return pointers into
the shared base's string data; btf__add_str(dst) then calls
btf__find_str(base) which can trigger btf_ensure_modifiable(base),
reallocating that string data and invalidating the pointer.
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.
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
tools/lib/bpf/btf.c | 38 ++++++++++++++++++++++++++++++--------
1 file changed, 30 insertions(+), 8 deletions(-)
diff --git a/tools/lib/bpf/btf.c b/tools/lib/bpf/btf.c
index 83fe79ffcb8f..0b0bb5cba22b 100644
--- a/tools/lib/bpf/btf.c
+++ b/tools/lib/bpf/btf.c
@@ -2004,24 +2004,41 @@ 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 && btf->base_btf &&
+ src_btf->base_btf != btf->base_btf)
+ return libbpf_err(-EINVAL);
+
+ 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))
return libbpf_err(-ENOMEM);
+ /* If dst has a base BTF, ensure it is modifiable now.
+ * btf_rewrite_str() resolves strings via btf__str_by_offset(src),
+ * which for split src may return pointers into dst's base string
+ * data. btf__add_str(dst) then calls btf__find_str(dst->base),
+ * which triggers btf_ensure_modifiable(base) and may reallocate
+ * the base string data, invalidating the pointer. Pre-emptively
+ * making the base modifiable avoids this use-after-free.
+ */
+ if (btf->base_btf && btf_ensure_modifiable(btf->base_btf))
+ return libbpf_err(-ENOMEM);
+
/* remember original strings section size if we have to roll back
* partial strings section changes
*/
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);
@@ -2074,11 +2091,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] 4+ messages in thread* [PATCH 2/3] bpftool: support merging multiple module BTFs in btf dump
2026-02-20 16:48 [PATCH 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
2026-02-20 16:48 ` [PATCH 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
@ 2026-02-20 16:48 ` Josef Bacik
2026-02-20 16:48 ` [PATCH 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources Josef Bacik
2 siblings, 0 replies; 4+ messages in thread
From: Josef Bacik @ 2026-02-20 16:48 UTC (permalink / raw)
To: bpf; +Cc: Claude Opus 4.6
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.
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
tools/bpf/bpftool/btf.c | 126 +++++++++++++++++++++++++++++++++++-----
1 file changed, 113 insertions(+), 13 deletions(-)
diff --git a/tools/bpf/bpftool/btf.c b/tools/bpf/bpftool/btf.c
index 946612029dee..be546a6d222f 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",
@@ -958,20 +959,118 @@ static int do_dump(int argc, char **argv)
NEXT_ARG();
} else if (is_prefix(src, "file")) {
const char sysfs_prefix[] = "/sys/kernel/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;
- }
+ files[nr_files++] = *argv;
NEXT_ARG();
+
+ /* Collect additional file arguments. Use strcmp (exact match)
+ * to avoid ambiguity with file paths that are prefixes of
+ * "file" (e.g., "./f").
+ */
+ while (argc && strcmp(*argv, "file") == 0) {
+ NEXT_ARG();
+ if (!REQ_ARGS(1)) {
+ err = -EINVAL;
+ goto done;
+ }
+ 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();
+ }
+
+ if (nr_files == 1) {
+ /* Single file — preserve existing behavior exactly */
+ if (!base_btf &&
+ strncmp(files[0], sysfs_prefix,
+ sizeof(sysfs_prefix) - 1) == 0 &&
+ strcmp(files[0], sysfs_vmlinux) != 0)
+ base = get_vmlinux_btf_from_sysfs();
+
+ 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 {
+ struct btf *vmlinux_base = base_btf;
+ struct btf *combined, *mod;
+ int j, ret;
+
+ /* Auto-detect vmlinux base from sysfs if needed */
+ if (!vmlinux_base) {
+ for (j = 0; j < nr_files; j++) {
+ if (strncmp(files[j], sysfs_prefix,
+ sizeof(sysfs_prefix) - 1) == 0 &&
+ strcmp(files[j], sysfs_vmlinux) != 0) {
+ base = get_vmlinux_btf_from_sysfs();
+ vmlinux_base = base;
+ break;
+ }
+ }
+ }
+ 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;
+ }
+
+ /* Filter out vmlinux from the file list */
+ for (j = 0; j < nr_files; j++) {
+ if (strcmp(files[j], sysfs_vmlinux) == 0) {
+ p_info("skipping %s (already loaded as base)",
+ sysfs_vmlinux);
+ memmove(&files[j], &files[j + 1],
+ (nr_files - j - 1) * sizeof(files[0]));
+ nr_files--;
+ j--;
+ }
+ }
+ if (nr_files == 0) {
+ p_err("no module BTF files to merge (all paths were vmlinux)");
+ err = -EINVAL;
+ goto done;
+ }
+
+ combined = btf__new_empty_split(vmlinux_base);
+ if (!combined) {
+ p_err("failed to create combined BTF: %s",
+ strerror(errno));
+ err = -errno;
+ goto done;
+ }
+
+ for (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);
+ err = -errno;
+ goto done;
+ }
+
+ 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);
+ err = ret;
+ goto done;
+ }
+ }
+
+ btf = combined;
+ }
} else {
err = -1;
p_err("unrecognized BTF source specifier: '%s'", src);
@@ -1445,7 +1544,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] 4+ messages in thread* [PATCH 3/3] selftests/bpf: add test for btf__add_btf() with split BTF sources
2026-02-20 16:48 [PATCH 0/3] libbpf/bpftool: support merging split BTFs Josef Bacik
2026-02-20 16:48 ` [PATCH 1/3] libbpf: support appending split BTF in btf__add_btf() Josef Bacik
2026-02-20 16:48 ` [PATCH 2/3] bpftool: support merging multiple module BTFs in btf dump Josef Bacik
@ 2026-02-20 16:48 ` Josef Bacik
2 siblings, 0 replies; 4+ messages in thread
From: Josef Bacik @ 2026-02-20 16:48 UTC (permalink / raw)
To: bpf; +Cc: Claude Opus 4.6
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.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
.../selftests/bpf/prog_tests/btf_write.c | 89 +++++++++++++++++++
1 file changed, 89 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..80353a545cdd 100644
--- a/tools/testing/selftests/bpf/prog_tests/btf_write.c
+++ b/tools/testing/selftests/bpf/prog_tests/btf_write.c
@@ -497,10 +497,99 @@ 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;
+
+ /* 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) */
+
+ /* 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 */
+
+ /* 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) should land at IDs 3,4 in combined */
+ 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) should be remapped to IDs 5,6 */
+ id = btf__add_btf(combined, split2);
+ if (!ASSERT_GE(id, 0, "add_split2"))
+ goto cleanup;
+ ASSERT_EQ(id, 5, "split2_first_id");
+
+ /* combined should have: base (2 types) + split1 (2) + split2 (2) = 6 types + void */
+ ASSERT_EQ(btf__type_cnt(combined), 7, "combined_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-4): 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 */
+
+ /* split2 types (IDs 5-6): remapped from 3,4 to 5,6 */
+ "[5] TYPEDEF 'int_ptr' type_id=2", /* base ptr=2, unchanged */
+ "[6] STRUCT 's2' size=8 vlen=1\n"
+ "\t'p' type_id=5 bits_offset=0"); /* split2 typedef: 3->5 */
+
+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] 4+ messages in thread