[RFC PATCH bpf-next v3 2/2] selftests/bpf: add tests for selective kmod BTF loading

From: Fuyu Zhao

Date: Wed Aug 19 2026 - 05:05:48 EST


The added test cases cover the following scenarios:

- Valid module: ensure loading succeeds when the required module BTF is
specified.
- Non-existent module: ensure loading fails when only a non-existent
module is specified and the required module BTF is unavailable.
- Duplicate module names: ensure duplicate entries are ignored.
- Unneeded module: ensure a loaded module whose BTF is not requested is
skipped.
- Missing count: ensure opening fails when kmod_btf_names is provided
without kmod_btf_names_cnt.

Signed-off-by: Fuyu Zhao <zhaofuyu@xxxxxxxx>
---
.../selftests/bpf/prog_tests/kmod_btfs.c | 196 ++++++++++++++++++
tools/testing/selftests/bpf/progs/kmod_btfs.c | 13 ++
2 files changed, 209 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/kmod_btfs.c
create mode 100644 tools/testing/selftests/bpf/progs/kmod_btfs.c

diff --git a/tools/testing/selftests/bpf/prog_tests/kmod_btfs.c b/tools/testing/selftests/bpf/prog_tests/kmod_btfs.c
new file mode 100644
index 000000000000..61d2845a731f
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/kmod_btfs.c
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <test_progs.h>
+#include "kmod_btfs.skel.h"
+
+static bool btf_skipped;
+static bool dup_ignored;
+
+static const char btf_skip_fmt[] =
+ "libbpf: skipping module BTF '%s', not in kmod_btf_names\n";
+static const char dup_ignore_fmt[] =
+ "libbpf: duplicate kmod BTF name '%s' ignored\n";
+
+static int libbpf_print_cb(enum libbpf_print_level level, const char *fmt,
+ va_list args)
+{
+ if (!strcmp(fmt, btf_skip_fmt)) {
+ if (!strcmp(va_arg(args, char *), "bpf_test_no_cfi"))
+ btf_skipped = true;
+ } else if (!strcmp(fmt, dup_ignore_fmt)) {
+ if (!strcmp(va_arg(args, char *), "bpf_testmod"))
+ dup_ignored = true;
+ }
+
+ return 0;
+}
+
+static void kmod_btfs_pass(void)
+{
+ struct kmod_btfs *skel = NULL;
+ int ret;
+ static const char *kmods[] = { "bpf_testmod" };
+
+ DECLARE_LIBBPF_OPTS(bpf_object_open_opts, opts,
+ .kmod_btf_names = kmods,
+ .kmod_btf_names_cnt = 1,
+ );
+
+ skel = kmod_btfs__open_opts(&opts);
+ if (!ASSERT_OK_PTR(skel, "kmod_btfs__open_opts"))
+ goto out;
+
+ ret = kmod_btfs__load(skel);
+ ASSERT_OK(ret, "kmod_btfs__load");
+out:
+ kmod_btfs__destroy(skel);
+}
+
+static void kmod_btfs_nonexist(void)
+{
+ struct kmod_btfs *skel = NULL;
+ int ret;
+ static const char *nonexist_kmods[] = { "module_nonexist" };
+
+ DECLARE_LIBBPF_OPTS(bpf_object_open_opts, opts,
+ .kmod_btf_names = nonexist_kmods,
+ .kmod_btf_names_cnt = 1,
+ );
+
+ /*
+ * Only "module_nonexist" is requested; since it does not exist in
+ * the kernel, no module BTF gets loaded. The BPF program then
+ * fails to load because the real needed module's BTF is unavailable.
+ */
+ skel = kmod_btfs__open_opts(&opts);
+ if (!ASSERT_OK_PTR(skel, "kmod_btfs__open_opts"))
+ goto out;
+
+ ret = kmod_btfs__load(skel);
+ ASSERT_ERR(ret, "kmod_btfs__load");
+
+out:
+ kmod_btfs__destroy(skel);
+}
+
+static void kmod_btfs_dup(void)
+{
+ struct kmod_btfs *skel = NULL;
+ libbpf_print_fn_t old_print_cb;
+ int ret;
+ static const char *dup_kmods[] = { "bpf_testmod", "bpf_testmod" };
+
+ /* duplicate entry on purpose: libbpf should dedupe it with a warning */
+ DECLARE_LIBBPF_OPTS(bpf_object_open_opts, opts,
+ .kmod_btf_names = dup_kmods,
+ .kmod_btf_names_cnt = 2,
+ );
+
+ dup_ignored = false;
+ old_print_cb = libbpf_set_print(libbpf_print_cb);
+
+ skel = kmod_btfs__open_opts(&opts);
+ if (!ASSERT_OK_PTR(skel, "kmod_btfs__open_opts"))
+ goto out;
+
+ ASSERT_TRUE(dup_ignored, "dup_ignored");
+
+ ret = kmod_btfs__load(skel);
+ ASSERT_OK(ret, "kmod_btfs__load");
+
+ kmod_btfs__destroy(skel);
+out:
+ libbpf_set_print(old_print_cb);
+}
+
+static void kmod_btfs_skip(void)
+{
+ struct kmod_btfs *skel = NULL;
+ libbpf_print_fn_t old_print_cb;
+ bool testmod_unloaded = false;
+ int ret;
+ static const char *kmods[] = { "bpf_testmod" };
+
+ DECLARE_LIBBPF_OPTS(bpf_object_open_opts, opts,
+ .kmod_btf_names = kmods,
+ .kmod_btf_names_cnt = 1,
+ );
+
+ btf_skipped = false;
+ old_print_cb = libbpf_set_print(libbpf_print_cb);
+
+ /*
+ * Reload bpf_testmod after bpf_test_no_cfi to ensure the unneeded
+ * module's BTF is enumerated first and skipped.
+ */
+ if (!ASSERT_OK(unload_bpf_testmod(false), "unload bpf_testmod"))
+ goto cleanup_print;
+ testmod_unloaded = true;
+
+ if (!ASSERT_OK(load_module("bpf_test_no_cfi.ko", false),
+ "load unneeded bpf_test_no_cfi module"))
+ goto cleanup_print;
+ if (!ASSERT_OK(load_bpf_testmod(false), "restore bpf_testmod"))
+ goto cleanup_module;
+ testmod_unloaded = false;
+
+ skel = kmod_btfs__open_opts(&opts);
+ if (!ASSERT_OK_PTR(skel, "kmod_btfs__open_opts"))
+ goto cleanup_module;
+
+ ret = kmod_btfs__load(skel);
+ if (!ASSERT_OK(ret, "kmod_btfs__load"))
+ goto cleanup_skel;
+
+ ASSERT_TRUE(btf_skipped, "btf_skipped");
+
+cleanup_skel:
+ kmod_btfs__destroy(skel);
+cleanup_module:
+ ASSERT_OK(unload_module("bpf_test_no_cfi", false),
+ "unload unneeded bpf_test_no_cfi module");
+cleanup_print:
+ if (testmod_unloaded)
+ ASSERT_OK(load_bpf_testmod(false),
+ "restore bpf_testmod on error path");
+ libbpf_set_print(old_print_cb);
+}
+
+static void kmod_btfs_no_cnt(void)
+{
+ struct kmod_btfs *skel = NULL;
+ static const char *kmods[] = { "bpf_testmod" };
+
+ /*
+ * kmod_btf_names set but kmod_btf_names_cnt not set:
+ * open must fail with -EINVAL.
+ */
+ DECLARE_LIBBPF_OPTS(bpf_object_open_opts, opts,
+ .kmod_btf_names = kmods,
+ );
+
+ skel = kmod_btfs__open_opts(&opts);
+ ASSERT_NULL(skel, "kmod_btfs__open_opts without kmod_btf_names_cnt");
+}
+
+void serial_test_kmod_btfs(void)
+{
+ if (!env.has_testmod) {
+ test__skip();
+ return;
+ }
+
+ if (test__start_subtest("kmod_btfs_pass"))
+ kmod_btfs_pass();
+
+ if (test__start_subtest("kmod_btfs_nonexist"))
+ kmod_btfs_nonexist();
+
+ if (test__start_subtest("kmod_btfs_dup"))
+ kmod_btfs_dup();
+
+ if (test__start_subtest("kmod_btfs_skip"))
+ kmod_btfs_skip();
+
+ if (test__start_subtest("kmod_btfs_no_cnt"))
+ kmod_btfs_no_cnt();
+}
diff --git a/tools/testing/selftests/bpf/progs/kmod_btfs.c b/tools/testing/selftests/bpf/progs/kmod_btfs.c
new file mode 100644
index 000000000000..b96860d60e0d
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/kmod_btfs.c
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+SEC("fentry/bpf_testmod_loop_test")
+int BPF_PROG(test_kmod_btfs)
+{
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
--
2.34.1