From: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
To: <bpf@vger.kernel.org>, <andrii@kernel.org>
Cc: <ast@kernel.org>, <martin.kelly@crowdstrike.com>,
<slava.imameev@crowdstrike.com>,
<linux-open-source@crowdstrike.com>
Subject: [PATCH bpf-next v4 7/7] selftests/bpf: Cover BPF program manual loading
Date: Mon, 21 Sep 2026 18:39:37 -0400 [thread overview]
Message-ID: <20260921223937.3203093-8-andrey.grodzovsky@crowdstrike.com> (raw)
In-Reply-To: <20260921223937.3203093-1-andrey.grodzovsky@crowdstrike.com>
Add dynamicload test covering the manual load/attach/detach/reload
cycle, declarative MANUAL via SEC("!...") and its imperative
override, bpf_object__prepare() alone being sufficient for manual
load, and a deferred load of a module BTF attach target. Also add
a signed_loader test verifying bpf_object__gen_loader() rejects
objects with a MANUAL program.
Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
.../selftests/bpf/prog_tests/dynamicload.c | 365 ++++++++++++++++++
.../selftests/bpf/prog_tests/signed_loader.c | 28 ++
.../selftests/bpf/progs/test_dynamicload.c | 54 +++
3 files changed, 447 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/dynamicload.c
create mode 100644 tools/testing/selftests/bpf/progs/test_dynamicload.c
diff --git a/tools/testing/selftests/bpf/prog_tests/dynamicload.c b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
new file mode 100644
index 000000000000..eaaa3e8bd54a
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
@@ -0,0 +1,365 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <test_progs.h>
+#include <time.h>
+#include "test_dynamicload.skel.h"
+
+#define READ_SZ 456
+
+/*
+ * prog4 is marked SEC("!...") in the source instead of being set
+ * imperatively; verify that an explicit bpf_program__set_load_strategy() call
+ * before load overrides the declarative default.
+ */
+static void dynamicload_verify_override(void)
+{
+ struct test_dynamicload *skel;
+ int err;
+
+ skel = test_dynamicload__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ err = bpf_program__set_load_strategy(skel->progs.prog4, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ if (!ASSERT_OK(err, "set_load_strategy_disabled"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog4),
+ BPF_PROG_LOAD_STRATEGY_DISABLED, "prog4_load_strategy_overridden"))
+ goto cleanup;
+
+ /*
+ * disable prog1/prog3 (also autoload by default) so this load only
+ * has to succeed for prog2 and the disabled prog4; prog2 loading is
+ * irrelevant to the assertion below and is left alone
+ */
+ err = bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ if (!ASSERT_OK(err, "set_load_strategy_disabled_prog1"))
+ goto cleanup;
+ err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ if (!ASSERT_OK(err, "set_load_strategy_disabled_prog3"))
+ goto cleanup;
+
+ err = test_dynamicload__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ /*
+ * prog4 was overridden to DISABLED, so its load_strategy != MANUAL
+ * and load() must reject it
+ */
+ err = bpf_program__load(skel->progs.prog4);
+ ASSERT_ERR(err, "load_after_override");
+
+cleanup:
+ test_dynamicload__destroy(skel);
+}
+
+/*
+ * prog4 is MANUAL via its SEC("!...") marker; verify that
+ * bpf_object__prepare() alone -- without ever calling bpf_object__load() --
+ * is sufficient for bpf_program__load() to succeed, since BTF
+ * loading, map creation, and relocation of MANUAL programs are all
+ * completed by prepare() already.
+ */
+static void dynamicload_verify_prepare_only(void)
+{
+ struct test_dynamicload *skel;
+ struct bpf_link *link = NULL;
+ int err;
+
+ skel = test_dynamicload__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ err = bpf_object__prepare(skel->obj);
+ if (!ASSERT_OK(err, "bpf_object__prepare"))
+ goto cleanup;
+
+ err = bpf_program__load(skel->progs.prog4);
+ if (!ASSERT_OK(err, "load_after_prepare"))
+ goto cleanup;
+
+ if (!ASSERT_GE(bpf_program__fd(skel->progs.prog4), 0, "prog4_fd_after_prepare"))
+ goto cleanup;
+
+ link = bpf_program__attach(skel->progs.prog4);
+ if (!ASSERT_OK_PTR(link, "attach_after_prepare"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog4_called, "prog4_called_after_prepare"))
+ goto cleanup;
+
+ err = bpf_link__destroy(link);
+ link = NULL;
+ if (!ASSERT_OK(err, "link_destroy_after_prepare"))
+ goto cleanup;
+
+ /*
+ * bpf_program__unload() is void now: for a MANUAL program it only
+ * closes the fd and retains func_info/line_info/subprogs so the
+ * program can be reloaded later.
+ */
+ bpf_program__unload(skel->progs.prog4);
+ ASSERT_LT(bpf_program__fd(skel->progs.prog4), 0, "prog4_fd_closed_after_unload");
+
+cleanup:
+ if (link)
+ bpf_link__destroy(link);
+ test_dynamicload__destroy(skel);
+}
+
+/*
+ * prog5 is disabled at parse time; resolve its attach target against
+ * module BTF via bpf_program__set_attach_target() before switching it
+ * to MANUAL and deferring its load past the bulk bpf_object__load().
+ * Regression test for the module BTF fd/array lifetime bug: without
+ * deferring the module BTF fd/array close for MANUAL programs, the fd
+ * cached in prog->attach_btf_obj_fd is closed by the bulk load's
+ * cleanup before this deferred load runs, causing a deterministic
+ * -EINVAL.
+ */
+static void dynamicload_verify_module_btf(void)
+{
+ struct test_dynamicload *skel;
+ struct bpf_link *link;
+ int err;
+
+ if (!env.has_testmod) {
+ test__skip();
+ return;
+ }
+
+ skel = test_dynamicload__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ err = bpf_program__set_attach_target(skel->progs.prog5, 0,
+ "bpf_testmod:bpf_testmod_test_read");
+ if (!ASSERT_OK(err, "set_attach_target"))
+ goto cleanup;
+
+ err = bpf_program__set_load_strategy(skel->progs.prog5, BPF_PROG_LOAD_STRATEGY_MANUAL);
+ if (!ASSERT_OK(err, "set_load_strategy_manual"))
+ goto cleanup;
+
+ /* keep the other autoload programs out of the way of this load */
+ bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ bpf_program__set_load_strategy(skel->progs.prog2, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
+
+ /*
+ * bulk load: prog5 itself is skipped (MANUAL), but this is where
+ * module BTF gets torn down if not correctly deferred
+ */
+ err = test_dynamicload__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ /*
+ * deferred load must still succeed: the module BTF fd cached above
+ * by set_attach_target() must still be a valid, open fd here
+ */
+ err = bpf_program__load(skel->progs.prog5);
+ if (!ASSERT_OK(err, "load_module_btf"))
+ goto cleanup;
+
+ link = bpf_program__attach(skel->progs.prog5);
+ if (!ASSERT_OK_PTR(link, "attach"))
+ goto cleanup;
+
+ ASSERT_OK(trigger_module_test_read(READ_SZ), "trigger_read");
+ ASSERT_EQ(skel->bss->prog5_sz, READ_SZ, "prog5_sz");
+
+ bpf_link__destroy(link);
+
+cleanup:
+ test_dynamicload__destroy(skel);
+}
+
+static void dynamicload_verify_main_cycle(void)
+{
+ struct bpf_link *link = NULL;
+ struct test_dynamicload *skel;
+ int err;
+
+ skel = test_dynamicload__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ /*
+ * the SEC("!...") prefix alone, with no imperative call, must set
+ * prog4's load strategy before it is ever touched below
+ */
+ if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog4),
+ BPF_PROG_LOAD_STRATEGY_MANUAL, "prog4_prefix_load_strategy"))
+ goto cleanup;
+ if (!ASSERT_FALSE(bpf_program__autoattach(skel->progs.prog4), "prog4_autoattach"))
+ goto cleanup;
+
+ /* don't load prog1 */
+ bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);
+
+ /* prog2 is autoload */
+ bpf_program__set_load_strategy(skel->progs.prog2, BPF_PROG_LOAD_STRATEGY_AUTO);
+
+ /* prog3 is manually loaded */
+ bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
+
+ err = test_dynamicload__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ err = test_dynamicload__attach(skel);
+ if (!ASSERT_OK(err, "skel_attach"))
+ goto cleanup;
+
+ /* trigger the BPF programs */
+ usleep(1);
+
+ ASSERT_FALSE(skel->bss->prog1_called, "prog1_called");
+ ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+ ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+ ASSERT_FALSE(skel->bss->prog4_called, "prog4_called");
+
+ /* prog1 is disabled for load */
+ err = bpf_program__load(skel->progs.prog1);
+ if (!ASSERT_ERR(err, "load_disabled"))
+ goto cleanup;
+
+ /* prog2 is autoload */
+ err = bpf_program__load(skel->progs.prog2);
+ if (!ASSERT_ERR(err, "load_autoload"))
+ goto cleanup;
+
+ /*
+ * bpf_program__unload() no longer rejects based on load strategy:
+ * calling it on prog2 (AUTO, currently loaded and attached) performs
+ * a full, irreversible unload instead of returning an error
+ */
+ bpf_program__unload(skel->progs.prog2);
+ ASSERT_LT(bpf_program__fd(skel->progs.prog2), 0, "prog2_fd_closed_after_unload");
+
+ /* reset the call flags */
+ skel->bss->prog2_called = false;
+ skel->bss->prog3_called = false;
+
+ usleep(1);
+
+ ASSERT_FALSE(skel->bss->prog1_called, "prog1_called");
+ ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+ ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+ /* load prog3 */
+ err = bpf_program__load(skel->progs.prog3);
+ if (!ASSERT_OK(err, "load"))
+ goto cleanup;
+
+ /* attach prog3 */
+ link = bpf_program__attach(skel->progs.prog3);
+ if (!ASSERT_OK_PTR(link, "attach"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called"))
+ goto cleanup;
+
+ /* detach prog3 as test_dynamicload__destroy doesn't detach manually loaded programs */
+ err = bpf_link__destroy(link);
+ link = NULL;
+ if (!ASSERT_OK(err, "link_destroy"))
+ goto cleanup;
+
+ /* reset the call flags after detach */
+ skel->bss->prog2_called = false;
+ skel->bss->prog3_called = false;
+
+ usleep(1);
+
+ ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+ ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+ /* unload prog3; MANUAL strategy means its data is retained for reload */
+ bpf_program__unload(skel->progs.prog3);
+
+ /* reload prog3 */
+ err = bpf_program__load(skel->progs.prog3);
+ if (!ASSERT_OK(err, "load_reload"))
+ goto cleanup;
+
+ /* reattach prog3 */
+ link = bpf_program__attach(skel->progs.prog3);
+ if (!ASSERT_OK_PTR(link, "reattach"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called_reattach"))
+ goto cleanup;
+
+ /* detach prog3 as test_dynamicload__destroy doesn't detach manually loaded programs */
+ err = bpf_link__destroy(link);
+ link = NULL;
+ if (!ASSERT_OK(err, "link_destroy_reattach"))
+ goto cleanup;
+
+ /* reset the call flags after detach */
+ skel->bss->prog2_called = false;
+ skel->bss->prog3_called = false;
+
+ usleep(1);
+
+ ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+ ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+ /*
+ * run prog4 (declaratively marked) through the same manual
+ * load/attach/trigger/detach/unload cycle as prog3
+ */
+ err = bpf_program__load(skel->progs.prog4);
+ if (!ASSERT_OK(err, "prog4_load"))
+ goto cleanup;
+
+ link = bpf_program__attach(skel->progs.prog4);
+ if (!ASSERT_OK_PTR(link, "prog4_attach"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog4_called, "prog4_called"))
+ goto cleanup;
+
+ err = bpf_link__destroy(link);
+ link = NULL;
+ if (!ASSERT_OK(err, "prog4_link_destroy"))
+ goto cleanup;
+
+ bpf_program__unload(skel->progs.prog4);
+
+ test_dynamicload__destroy(skel);
+ return;
+
+cleanup:
+ if (link)
+ bpf_link__destroy(link);
+ test_dynamicload__destroy(skel);
+}
+
+void test_dynamicload(void)
+{
+ if (test__start_subtest("main_cycle"))
+ dynamicload_verify_main_cycle();
+
+ if (test__start_subtest("verify_override"))
+ dynamicload_verify_override();
+
+ if (test__start_subtest("verify_prepare_only"))
+ dynamicload_verify_prepare_only();
+
+ if (test__start_subtest("verify_module_btf"))
+ dynamicload_verify_module_btf();
+}
+
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index a0f93756e717..0648816e3d50 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -2168,6 +2168,33 @@ static void signed_module_kfunc_rejected(void)
run_setup("cleanup", dir);
}
+/*
+ * a program marked MANUAL is invisible to gen_loader's program count (it is
+ * skipped by bpf_object_load_progs()), so bpf_object__gen_loader() must
+ * reject the whole object up front instead of silently generating a
+ * skeleton whose prog_fd slots no longer line up with the object's programs
+ */
+static void manual_prog_rejected(void)
+{
+ LIBBPF_OPTS(gen_loader_opts, gopts, .gen_hash = true);
+ struct test_signed_loader *skel;
+ int err;
+
+ skel = test_signed_loader__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ err = bpf_program__set_load_strategy(skel->progs.probe, BPF_PROG_LOAD_STRATEGY_MANUAL);
+ if (!ASSERT_OK(err, "set_load_strategy_manual"))
+ goto cleanup;
+
+ err = bpf_object__gen_loader(skel->obj, &gopts);
+ ASSERT_ERR(err, "gen_loader_rejected");
+
+cleanup:
+ test_signed_loader__destroy(skel);
+}
+
enum subtest_boot {
BOOT_ANY,
BOOT_SEALED,
@@ -2211,6 +2238,7 @@ static const struct {
{ "signed_map_by_fd_rejected", signed_map_by_fd_rejected, BOOT_SEALED },
{ "signed_sparse_fd_array_rejected", signed_sparse_fd_array_rejected, BOOT_SEALED },
{ "bpf_keyring_provisioned", bpf_keyring_provisioned, BOOT_UNSEALED },
+ { "manual_prog_rejected", manual_prog_rejected, BOOT_ANY },
};
void test_signed_loader(void)
diff --git a/tools/testing/selftests/bpf/progs/test_dynamicload.c b/tools/testing/selftests/bpf/progs/test_dynamicload.c
new file mode 100644
index 000000000000..6cdbfc21ca37
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_dynamicload.c
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+bool prog1_called = false;
+bool prog2_called = false;
+bool prog3_called = false;
+bool prog4_called = false;
+__u32 prog5_sz = 0;
+
+SEC("raw_tp/sys_enter")
+int prog1(const void *ctx)
+{
+ prog1_called = true;
+ return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog2(const void *ctx)
+{
+ prog2_called = true;
+ return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog3(const void *ctx)
+{
+ prog3_called = true;
+ return 0;
+}
+
+SEC("!raw_tp/sys_enter")
+int prog4(const void *ctx)
+{
+ prog4_called = true;
+ return 0;
+}
+
+/*
+ * disabled at parse time; its attach target is resolved against module
+ * BTF via bpf_program__set_attach_target() before it is switched to
+ * MANUAL and loaded
+ */
+SEC("?fentry")
+int BPF_PROG(prog5, struct file *file, struct kobject *kobj,
+ const struct bin_attribute *bin_attr, char *buf, loff_t off, size_t len)
+{
+ prog5_sz = len;
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
--
2.34.1
next prev parent reply other threads:[~2026-09-21 22:39 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-21 22:39 [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
2026-09-21 22:39 ` [PATCH bpf-next v4 1/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
2026-09-21 23:21 ` bot+bpf-ci
2026-09-21 22:39 ` [PATCH bpf-next v4 2/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
2026-09-21 23:03 ` sashiko-bot
2026-09-22 23:51 ` Andrey Grodzovsky
2026-09-21 22:39 ` [PATCH bpf-next v4 3/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
2026-09-21 23:12 ` sashiko-bot
2026-09-21 23:21 ` bot+bpf-ci
2026-09-21 22:39 ` [PATCH bpf-next v4 4/7] libbpf: Reject gen_loader for objects with already-manual programs Andrey Grodzovsky
2026-09-21 22:39 ` [PATCH bpf-next v4 5/7] libbpf: Version bpf_program__set_autoattach() ABI change Andrey Grodzovsky
2026-09-21 23:31 ` sashiko-bot
2026-09-21 22:39 ` [PATCH bpf-next v4 6/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
2026-09-21 23:37 ` sashiko-bot
2026-09-21 22:39 ` Andrey Grodzovsky [this message]
2026-09-22 1:37 ` [PATCH bpf-next v4 0/7] libbpf: BPF program manual loading Alexei Starovoitov
2026-09-22 14:34 ` Andrey Grodzovsky
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260921223937.3203093-8-andrey.grodzovsky@crowdstrike.com \
--to=andrey.grodzovsky@crowdstrike.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=linux-open-source@crowdstrike.com \
--cc=martin.kelly@crowdstrike.com \
--cc=slava.imameev@crowdstrike.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox