* [PATCH bpf-next v3 0/6] libbpf: BPF program manual loading
@ 2026-09-17 20:30 Andrey Grodzovsky
2026-09-17 20:30 ` [PATCH bpf-next v3 1/6] libbpf: BPF program load strategy enum Andrey Grodzovsky
` (5 more replies)
0 siblings, 6 replies; 16+ messages in thread
From: Andrey Grodzovsky @ 2026-09-17 20:30 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source
This series was originally posted in January 2025 [1] as a two-patch RFC
introducing a per-program tri-state load strategy (disabled/auto/dynamic)
to let large BPF applications load a subset of their programs lazily,
after the initial bpf_object load. v2 [2] kept the same per-program enum
and added a bool-compatible bpf_program__set_autoload() shim plus a
declarative BTF decl_tag marker. This v3 renames DYNAMIC to MANUAL,
replaces the decl_tag marker with a SEC("!...") prefix, relaxes the
load gate to right after bpf_object__prepare(), adds support for
kernel module attach targets, and fixes bugs and addresses comments
identified in v2's review (see the changelog below).
Motivation:
Security tools built on top of libbpf commonly ship as a single large
BPF object containing many programs, only a subset of which are needed
on any given system -- the rest are gated on optional features or on
kernel/runtime capabilities that vary across deployments. For this class
of application, per-program manual loading helps in two ways:
- it shortens the initial load, since only the programs actually
needed for the running configuration are loaded and attached up
front instead of the whole object; and
- it lets a single failing program be unloaded and possibly reloaded
(or a feature toggled at runtime) without tearing down and reloading
the entire bpf_object.
Both reduce the time window during which the tool is partially loaded or
inactive -- for a security tool specifically, that is also the time
window during which the system it protects is unprotected.
We have been running this with our internal fork of libbpf in production
for about a year. Depending on kernel and configuration support, around
250 of our programs are potentially loadable on any given system; with
manual loading, only around 90 of those are actually loaded by default,
growing to the full 250 only when every optional feature is enabled.
Cutting the default set of loaded programs from 250 down to 90 measurably
reduces load time (we observed ~60% reduction in our own measurements),
and since unload time scales with the number of loaded programs, it
correspondingly shortens unload time as well -- which matters, since
slow unload can delay system shutdown. We believe this functionality
would benefit other libbpf consumers with similarly large, modular BPF
applications.
Patch overview:
1/6: replace bpf_program's boolean autoload field with an enum
(bpf_prog_load_strategy: DISABLED/AUTO), so a third state can be
added later without an ABI break
2/6: add BPF_PROG_LOAD_STRATEGY_MANUAL and
bpf_program__load_manually()/unload_manually(), letting a program
be loaded and attached independently of the bulk bpf_object
load/attach pass, and reloaded/reattached multiple times
3/6: let a program mark itself MANUAL from its own section name via a
SEC("!...") prefix, instead of requiring an imperative
bpf_program__set_load_strategy() call, mirroring the existing
SEC("?...") convention
4/6: reject bpf_object__gen_loader() for an object with a MANUAL
program, which would otherwise silently corrupt every later
program's generated prog_fd slot
5/6: selftest covering every load-strategy transition
(DISABLED/AUTO/MANUAL), bpf_program__set_autoload()'s bool
compatibility, and autoattach restoration
6/6: selftest covering the manual load/attach/detach/reload cycle, the
declarative SEC("!...") marker, prepare()-only load sufficiency,
a module BTF deferred load, and gen_loader rejection
Changes since v2 [2]:
- Rename load_type -> load_strategy, DYNAMIC -> MANUAL throughout the
API surface (Andrii)
- Move the declarative marker from a BTF decl_tag to a SEC("!...")
prefix (Andrii)
- Relax bpf_program__load_manually()'s gate from OBJ_LOADED to
OBJ_PREPARED (Andrii)
- Compute has_manual_progs upfront in bpf_object_prepare_progs()
instead of tracking it incrementally (Andrii)
- Drop the bool->enum conversion of bpf_program__set_autoload()
entirely; it stays bool-signature and becomes a thin, unconditional
forwarder to set_load_strategy() (Andrii: "let's just leave it be")
- Fix set_load_strategy()'s DYNAMIC->AUTO/DISABLED transition never
restoring prog->autoattach (bot+bpf-ci)
- Drop the premature SEC_ATTACH_BTF early-reject in
set_load_strategy(MANUAL) (bot+bpf-ci and Andrii independently)
- Reject gen_loader for objects with an already-MANUAL program: a
MANUAL program is invisible to gen_loader's program count, silently
shifting every later program's generated prog_fd slot (bot+bpf-ci)
- Reject load_manually() with -EBUSY when prog->fd is already open,
instead of silently overwriting it and leaking the previous fd
on repeat load without an intervening unload (sashiko-bot)
- Reject bpf_program__set_autoattach() for a MANUAL-strategy program
(its signature changes from void to int accordingly), since setting
autoattach directly would corrupt the value saved_autoattach
restores on the next transition back to AUTO/DISABLED
- Wrap load_manually()'s raw error via libbpf_err() and fix a NULL-
check ordering bug (sashiko-bot); drop a redundant !obj check --
prog->obj is an invariant elsewhere in libbpf.c (Andrii)
- Drop the extra_log_level parameter from load_manually(); callers
already have bpf_program__set_log_level() (Andrii)
- Expand selftest coverage: a prepare()-only load-sufficiency test
with attach + execution verification (closes the gap behind
Andrii's OBJ_PREPARED question above), a module BTF deferred-load
regression test, and a gen_loader-rejection test
[1] https://lore.kernel.org/bpf/20250122215206.59859-1-slava.imameev@crowdstrike.com/
[2] https://lore.kernel.org/bpf/20260826181825.1826541-1-andrey.grodzovsky@crowdstrike.com/
Andrey Grodzovsky (3):
libbpf: Support declarative manual load via SEC("!...") prefix
libbpf: Reject gen_loader for objects with already-manual programs
selftests/bpf: Cover BPF program manual loading
Slava Imameev (3):
libbpf: BPF program load strategy enum
libbpf: BPF programs manual loading and attaching
selftests/bpf: Cover BPF program load strategy transitions
tools/lib/bpf/libbpf.c | 234 +++++++++---
tools/lib/bpf/libbpf.h | 40 +-
tools/lib/bpf/libbpf.map | 4 +
.../selftests/bpf/prog_tests/dynamicload.c | 350 ++++++++++++++++++
.../selftests/bpf/prog_tests/load_type.c | 157 ++++++++
.../selftests/bpf/prog_tests/signed_loader.c | 28 ++
.../selftests/bpf/progs/test_dynamicload.c | 54 +++
.../selftests/bpf/progs/test_load_type.c | 31 ++
8 files changed, 854 insertions(+), 44 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/dynamicload.c
create mode 100644 tools/testing/selftests/bpf/prog_tests/load_type.c
create mode 100644 tools/testing/selftests/bpf/progs/test_dynamicload.c
create mode 100644 tools/testing/selftests/bpf/progs/test_load_type.c
--
2.34.1
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH bpf-next v3 1/6] libbpf: BPF program load strategy enum
2026-09-17 20:30 [PATCH bpf-next v3 0/6] libbpf: BPF program manual loading Andrey Grodzovsky
@ 2026-09-17 20:30 ` Andrey Grodzovsky
2026-09-17 21:16 ` bot+bpf-ci
2026-09-17 20:30 ` [PATCH bpf-next v3 2/6] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
` (4 subsequent siblings)
5 siblings, 1 reply; 16+ messages in thread
From: Andrey Grodzovsky @ 2026-09-17 20:30 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source
From: Slava Imameev <slava.imameev@crowdstrike.com>
Replacing the boolean field with an enum simplifies the addition
of new load strategies. Currently, the bpf_program structure defines
the autoload behavior using a boolean field. This field is now
replaced with an enum, allowing new BPF program loading strategies
to be introduced by extending the enum value range.
Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 50 +++++++++++++++++++++++++---------------
tools/lib/bpf/libbpf.h | 33 ++++++++++++++++++++++++++
tools/lib/bpf/libbpf.map | 2 ++
3 files changed, 66 insertions(+), 19 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 613afae26519..bd510a4e4941 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -494,7 +494,7 @@ struct bpf_program {
struct bpf_object *obj;
int fd;
- bool autoload;
+ enum bpf_prog_load_strategy load_strategy;
bool autoattach;
bool sym_global;
bool mark_btf_static;
@@ -872,11 +872,11 @@ bpf_object__init_prog(struct bpf_object *obj, struct bpf_program *prog,
* autoload set to false.
*/
if (sec_name[0] == '?') {
- prog->autoload = false;
+ prog->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;
/* from now on forget there was ? in section name */
sec_name++;
} else {
- prog->autoload = true;
+ prog->load_strategy = BPF_PROG_LOAD_STRATEGY_AUTO;
}
prog->autoattach = true;
@@ -1163,7 +1163,8 @@ static int bpf_object_adjust_struct_ops_autoload(struct bpf_object *obj)
}
}
if (use_cnt)
- prog->autoload = should_load;
+ prog->load_strategy = should_load ? BPF_PROG_LOAD_STRATEGY_AUTO
+ : BPF_PROG_LOAD_STRATEGY_DISABLED;
}
return 0;
@@ -1248,7 +1249,7 @@ static int bpf_map__init_kern_struct_ops(struct bpf_map *map)
* then bpf_object_adjust_struct_ops_autoload() will update its
* autoload accordingly.
*/
- st_ops->progs[i]->autoload = false;
+ st_ops->progs[i]->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;
st_ops->progs[i] = NULL;
}
@@ -1287,7 +1288,7 @@ static int bpf_map__init_kern_struct_ops(struct bpf_map *map)
* if user replaced it with another program or NULL
*/
if (st_ops->progs[i] && st_ops->progs[i] != prog)
- st_ops->progs[i]->autoload = false;
+ st_ops->progs[i]->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;
/* Update the value from the shadow type */
st_ops->progs[i] = prog;
@@ -3587,7 +3588,7 @@ static bool obj_needs_vmlinux_btf(const struct bpf_object *obj)
}
bpf_object__for_each_program(prog, obj) {
- if (!prog->autoload)
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)
continue;
if (prog_needs_vmlinux_btf(prog))
return true;
@@ -6181,7 +6182,7 @@ bpf_object__relocate_core(struct bpf_object *obj, const char *targ_btf_path)
/* no need to apply CO-RE relocation if the program is
* not going to be loaded
*/
- if (!prog->autoload)
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)
continue;
/* adjust insn_idx from section frame of reference to the local
@@ -7525,7 +7526,7 @@ static int bpf_object__relocate(struct bpf_object *obj, const char *targ_btf_pat
*/
if (prog_is_subprog(obj, prog))
continue;
- if (!prog->autoload)
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)
continue;
err = bpf_object__relocate_calls(obj, prog);
@@ -7561,7 +7562,7 @@ static int bpf_object__relocate(struct bpf_object *obj, const char *targ_btf_pat
prog = &obj->programs[i];
if (prog_is_subprog(obj, prog))
continue;
- if (!prog->autoload)
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_DISABLED)
continue;
/* Process data relos for main programs */
@@ -8400,8 +8401,8 @@ bpf_object__load_progs(struct bpf_object *obj, int log_level)
prog = &obj->programs[i];
if (prog_is_subprog(obj, prog))
continue;
- if (!prog->autoload) {
- pr_debug("prog '%s': skipped loading\n", prog->name);
+ if (prog->load_strategy != BPF_PROG_LOAD_STRATEGY_AUTO) {
+ pr_debug("prog '%s': skipped auto-loading\n", prog->name);
continue;
}
prog->log_level |= log_level;
@@ -9802,16 +9803,13 @@ const char *bpf_program__section_name(const struct bpf_program *prog)
bool bpf_program__autoload(const struct bpf_program *prog)
{
- return prog->autoload;
+ return prog->load_strategy == BPF_PROG_LOAD_STRATEGY_AUTO;
}
int bpf_program__set_autoload(struct bpf_program *prog, bool autoload)
{
- if (prog->obj->state >= OBJ_LOADED)
- return libbpf_err(-EINVAL);
-
- prog->autoload = autoload;
- return 0;
+ return bpf_program__set_load_strategy(prog,
+ autoload ? BPF_PROG_LOAD_STRATEGY_AUTO : BPF_PROG_LOAD_STRATEGY_DISABLED);
}
bool bpf_program__autoattach(const struct bpf_program *prog)
@@ -15125,7 +15123,7 @@ int bpf_object__attach_skeleton(struct bpf_object_skeleton *s)
struct bpf_program *prog = *prog_skel->prog;
struct bpf_link **link = prog_skel->link;
- if (!prog->autoload || !prog->autoattach)
+ if (prog->load_strategy != BPF_PROG_LOAD_STRATEGY_AUTO || !prog->autoattach)
continue;
/* auto-attaching not supported for this program */
@@ -15235,3 +15233,17 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
free(s->progs);
free(s);
}
+
+int bpf_program__set_load_strategy(struct bpf_program *prog, enum bpf_prog_load_strategy strategy)
+{
+ if (prog->obj->state >= OBJ_LOADED)
+ return libbpf_err(-EINVAL);
+
+ prog->load_strategy = strategy;
+ return 0;
+}
+
+enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program *prog)
+{
+ return prog->load_strategy;
+}
diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
index 48266e752223..cdf300443f19 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -2100,6 +2100,39 @@ LIBBPF_API int libbpf_unregister_prog_handler(int handler_id);
*/
LIBBPF_API int bpf_program__clone(struct bpf_program *prog, const struct bpf_prog_load_opts *opts);
+/**
+ * The program load strategy:
+ *
+ * - BPF_PROG_LOAD_STRATEGY_DISABLED: the program is not loaded.
+ * - BPF_PROG_LOAD_STRATEGY_AUTO: the program is autoloaded when the bpf_object is loaded.
+ */
+enum bpf_prog_load_strategy {
+ BPF_PROG_LOAD_STRATEGY_DISABLED = 0,
+ BPF_PROG_LOAD_STRATEGY_AUTO,
+};
+
+/**
+ * @brief **bpf_program__set_load_strategy()** sets the load strategy of a
+ * BPF program, controlling whether and when it gets loaded into the kernel.
+ *
+ * Can only be called before the enclosing bpf_object is loaded.
+ *
+ * @param prog BPF program to update
+ * @param strategy new load strategy for the program
+ * @return 0 on success; negative error code if the object was already loaded
+ */
+LIBBPF_API int bpf_program__set_load_strategy(struct bpf_program *prog,
+ enum bpf_prog_load_strategy strategy);
+
+/**
+ * @brief **bpf_program__load_strategy()** returns the current load strategy
+ * of a BPF program.
+ *
+ * @param prog BPF program to query
+ * @return current load strategy of the program
+ */
+LIBBPF_API enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program *prog);
+
#ifdef __cplusplus
} /* extern "C" */
#endif
diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
index a811a1b3a085..f0bf6a846149 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -462,6 +462,8 @@ LIBBPF_1.8.0 {
bpf_program__attach_tracing_multi;
bpf_program__clear_flags;
bpf_program__clone;
+ bpf_program__load_strategy;
+ bpf_program__set_load_strategy;
btf__find_by_name_kind_own;
btf__new_empty_opts;
} LIBBPF_1.7.0;
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH bpf-next v3 2/6] libbpf: BPF programs manual loading and attaching
2026-09-17 20:30 [PATCH bpf-next v3 0/6] libbpf: BPF program manual loading Andrey Grodzovsky
2026-09-17 20:30 ` [PATCH bpf-next v3 1/6] libbpf: BPF program load strategy enum Andrey Grodzovsky
@ 2026-09-17 20:30 ` Andrey Grodzovsky
2026-09-17 20:41 ` sashiko-bot
2026-09-17 21:34 ` bot+bpf-ci
2026-09-17 20:30 ` [PATCH bpf-next v3 3/6] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
` (3 subsequent siblings)
5 siblings, 2 replies; 16+ messages in thread
From: Andrey Grodzovsky @ 2026-09-17 20:30 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source
From: Slava Imameev <slava.imameev@crowdstrike.com>
BPF programs designated as manually loaded can be loaded and
attached independently after the initial bpf_object loading and
attaching.
These programs can also be reloaded and reattached multiple times,
enabling more flexible management of a resident BPF program set.
A key motivation for this feature is to reduce load times for
utilities that include hundreds of BPF programs. When the selection
of a resident BPF program set cannot be determined at the time of
bpf_object loading and attaching, all BPF programs would otherwise
need to be marked as autoload, leading to unnecessary overhead.
This patch addresses that inefficiency.
A manual-strategy program is loaded via bpf_program__load_manually()
and unloaded via bpf_program__unload_manually(), gated on
bpf_object__prepare() having already run (BTF loaded, maps created,
relocations applied) rather than requiring a full bpf_object__load().
Manual programs are skipped by the object's own autoload pass.
Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 159 +++++++++++++++++++++++++++++++++------
tools/lib/bpf/libbpf.h | 7 +-
tools/lib/bpf/libbpf.map | 2 +
3 files changed, 142 insertions(+), 26 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index bd510a4e4941..acb8caffa442 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -496,6 +496,7 @@ struct bpf_program {
int fd;
enum bpf_prog_load_strategy load_strategy;
bool autoattach;
+ bool saved_autoattach;
bool sym_global;
bool mark_btf_static;
enum bpf_prog_type type;
@@ -725,6 +726,7 @@ struct bpf_object {
bool has_subcalls;
bool has_rodata;
+ bool has_manual_progs;
struct bpf_gen *gen_loader;
@@ -8058,7 +8060,7 @@ static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog
log_buf = prog->log_buf;
log_buf_size = prog->log_size;
own_log_buf = false;
- } else if (obj->log_buf) {
+ } else if (obj->log_buf && prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL) {
log_buf = obj->log_buf;
log_buf_size = obj->log_size;
own_log_buf = false;
@@ -8405,6 +8407,7 @@ bpf_object__load_progs(struct bpf_object *obj, int log_level)
pr_debug("prog '%s': skipped auto-loading\n", prog->name);
continue;
}
+
prog->log_level |= log_level;
if (obj->gen_loader)
@@ -8430,6 +8433,8 @@ static int bpf_object_prepare_progs(struct bpf_object *obj)
for (i = 0; i < obj->nr_programs; i++) {
prog = &obj->programs[i];
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
+ obj->has_manual_progs = true;
err = bpf_object__sanitize_prog(obj, prog);
if (err)
return err;
@@ -9063,33 +9068,50 @@ static void bpf_object_unpin(struct bpf_object *obj)
bpf_map__unpin(&obj->maps[i], NULL);
}
-static void bpf_object_cleanup_btf(struct bpf_object *obj)
+static void bpf_object_cleanup_btf(struct bpf_object *obj, bool force)
{
int i;
- /* clean up module BTFs */
- for (i = 0; i < obj->btf_module_cnt; i++) {
- close(obj->btf_modules[i].fd);
- btf__free(obj->btf_modules[i].btf);
- free(obj->btf_modules[i].name);
+ /*
+ * Module BTF fds may still be borrowed (via fd_array,
+ * attach_btf_obj_fd, or baked into relocated instructions) by
+ * programs that have not been manually loaded yet, so defer
+ * closing them in that case to the end of the object lifetime,
+ * unless the caller forces immediate cleanup.
+ */
+ if (force || !obj->has_manual_progs) {
+ for (i = 0; i < obj->btf_module_cnt; i++) {
+ close(obj->btf_modules[i].fd);
+ btf__free(obj->btf_modules[i].btf);
+ free(obj->btf_modules[i].name);
+ }
+ obj->btf_module_cnt = 0;
+ obj->btf_module_cap = 0;
+ obj->btf_modules_loaded = false;
+ zfree(&obj->btf_modules);
}
- obj->btf_module_cnt = 0;
- obj->btf_module_cap = 0;
- obj->btf_modules_loaded = false;
- zfree(&obj->btf_modules);
- /* clean up vmlinux BTF */
- btf__free(obj->btf_vmlinux);
- obj->btf_vmlinux = NULL;
+ /*
+ * The btf_vmlinux data is needed for manually loaded programs,
+ * so defer freeing it in that case to the end of the object lifetime.
+ */
+ if (force || !obj->has_manual_progs) {
+ btf__free(obj->btf_vmlinux);
+ obj->btf_vmlinux = NULL;
+ }
}
-static void bpf_object_post_load_cleanup(struct bpf_object *obj)
+static void bpf_object_post_load_cleanup(struct bpf_object *obj, bool force)
{
- /* clean up fd_array */
- zfree(&obj->fd_array);
+ /*
+ * The fd array is needed for manually loaded programs,
+ * so defer freeing it in that case to the end of the object lifetime.
+ */
+ if (force || !obj->has_manual_progs || !obj->fd_array_cnt)
+ zfree(&obj->fd_array);
/* clean up BTF */
- bpf_object_cleanup_btf(obj);
+ bpf_object_cleanup_btf(obj, force);
}
static int bpf_object_prepare(struct bpf_object *obj, const char *target_btf_path)
@@ -9164,7 +9186,7 @@ static int bpf_object_load(struct bpf_object *obj, int extra_log_level, const ch
err = bpf_gen__finish(obj->gen_loader, obj->nr_programs, obj->nr_maps);
}
- bpf_object_post_load_cleanup(obj);
+ bpf_object_post_load_cleanup(obj, false);
obj->state = OBJ_LOADED; /* doesn't matter if successfully or not */
if (err) {
@@ -9632,7 +9654,7 @@ void bpf_object__close(struct bpf_object *obj)
* bpf_object__load(), we need to clean up stuff that is normally
* cleaned up at the end of loading step
*/
- bpf_object_post_load_cleanup(obj);
+ bpf_object_post_load_cleanup(obj, true);
usdt_manager_free(obj->usdt_man);
obj->usdt_man = NULL;
@@ -9641,7 +9663,6 @@ void bpf_object__close(struct bpf_object *obj)
bpf_object__elf_finish(obj);
bpf_object_unload(obj);
btf__free(obj->btf);
- btf__free(obj->btf_vmlinux);
btf_ext__free(obj->btf_ext);
for (i = 0; i < obj->nr_maps; i++)
@@ -9817,9 +9838,13 @@ bool bpf_program__autoattach(const struct bpf_program *prog)
return prog->autoattach;
}
-void bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach)
+int bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach)
{
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
+ return libbpf_err(-EINVAL);
+
prog->autoattach = autoattach;
+ return 0;
}
const struct bpf_insn *bpf_program__insns(const struct bpf_program *prog)
@@ -12743,7 +12768,7 @@ static int collect_func_ids_by_glob(const struct bpf_program *prog, const char *
err = collect_btf_func_ids_by_glob(btf, pattern, ids);
cleanup:
- bpf_object_cleanup_btf(obj);
+ bpf_object_cleanup_btf(obj, false);
return err;
}
@@ -15236,10 +15261,48 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
int bpf_program__set_load_strategy(struct bpf_program *prog, enum bpf_prog_load_strategy strategy)
{
- if (prog->obj->state >= OBJ_LOADED)
+ struct bpf_object *obj = prog->obj;
+
+ if (obj->state >= OBJ_PREPARED)
+ return libbpf_err(-EINVAL);
+
+ if (strategy == prog->load_strategy)
+ return 0;
+
+ switch (strategy) {
+ case BPF_PROG_LOAD_STRATEGY_DISABLED:
+ case BPF_PROG_LOAD_STRATEGY_AUTO:
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
+ prog->autoattach = prog->saved_autoattach;
+ prog->load_strategy = strategy;
+ break;
+ case BPF_PROG_LOAD_STRATEGY_MANUAL:
+ /*
+ * Manually-loaded programs are not supported for gen_loader.
+ * This is because bpf_object_load_prog is not called for
+ * manually-loaded programs, so such programs are not visible
+ * to gen_loader. For this reason, prevent calling
+ * bpf_program__set_load_strategy(MANUAL) when gen_loader was
+ * used to generate a BPF object loader.
+ * A gen_loader implementation is being called for autoloaded
+ * programs and defines its own model for loading BPF programs.
+ * To pass a BPF program to gen_loader, set the program's load strategy
+ * to LD_AUTOLOAD.
+ */
+ if (obj->gen_loader)
+ return libbpf_err(-EOPNOTSUPP);
+
+ if (prog_is_subprog(obj, prog))
+ return libbpf_err(-EINVAL);
+
+ prog->saved_autoattach = prog->autoattach;
+ prog->load_strategy = BPF_PROG_LOAD_STRATEGY_MANUAL;
+ prog->autoattach = false;
+ break;
+ default:
return libbpf_err(-EINVAL);
+ }
- prog->load_strategy = strategy;
return 0;
}
@@ -15247,3 +15310,49 @@ enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program
{
return prog->load_strategy;
}
+
+/*
+ * This function must be called after bpf_object__prepare (or
+ * bpf_object__load, which calls bpf_object__prepare internally).
+ * Manually-loaded program data is initialized on object prepare.
+ * Post-prepare initialization is not supported.
+ */
+int
+bpf_program__load_manually(struct bpf_program *prog)
+{
+ int err;
+ struct bpf_object *obj = prog->obj;
+
+ if (obj->state < OBJ_PREPARED)
+ return libbpf_err(-EINVAL);
+
+ if (prog_is_subprog(obj, prog) || prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)
+ return libbpf_err(-EINVAL);
+
+ if (prog->fd >= 0)
+ return libbpf_err(-EBUSY);
+
+ err = bpf_object_load_prog(obj, prog, prog->insns, prog->insns_cnt,
+ obj->license, obj->kern_version, &prog->fd);
+ if (err) {
+ pr_warn("prog '%s': failed to load manually: %d\n", prog->name, err);
+ return libbpf_err(err);
+ }
+
+ return 0;
+}
+
+int bpf_program__unload_manually(struct bpf_program *prog)
+{
+ int err;
+
+ if (prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)
+ return libbpf_err(-EINVAL);
+
+ /*
+ * Close the file descriptor but retain the program's data to
+ * support reloading the program if it is required again.
+ */
+ err = zclose(prog->fd);
+ return err ? libbpf_err(-errno) : 0;
+}
diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
index cdf300443f19..5914e5001b97 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -378,7 +378,7 @@ LIBBPF_API const char *bpf_program__section_name(const struct bpf_program *prog)
LIBBPF_API bool bpf_program__autoload(const struct bpf_program *prog);
LIBBPF_API int bpf_program__set_autoload(struct bpf_program *prog, bool autoload);
LIBBPF_API bool bpf_program__autoattach(const struct bpf_program *prog);
-LIBBPF_API void bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach);
+LIBBPF_API int bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach);
struct bpf_insn;
@@ -2105,10 +2105,12 @@ LIBBPF_API int bpf_program__clone(struct bpf_program *prog, const struct bpf_pro
*
* - BPF_PROG_LOAD_STRATEGY_DISABLED: the program is not loaded.
* - BPF_PROG_LOAD_STRATEGY_AUTO: the program is autoloaded when the bpf_object is loaded.
+ * - BPF_PROG_LOAD_STRATEGY_MANUAL: the program is loaded and attached manually.
*/
enum bpf_prog_load_strategy {
BPF_PROG_LOAD_STRATEGY_DISABLED = 0,
BPF_PROG_LOAD_STRATEGY_AUTO,
+ BPF_PROG_LOAD_STRATEGY_MANUAL,
};
/**
@@ -2133,6 +2135,9 @@ LIBBPF_API int bpf_program__set_load_strategy(struct bpf_program *prog,
*/
LIBBPF_API enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program *prog);
+LIBBPF_API int bpf_program__load_manually(struct bpf_program *prog);
+LIBBPF_API int bpf_program__unload_manually(struct bpf_program *prog);
+
#ifdef __cplusplus
} /* extern "C" */
#endif
diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
index f0bf6a846149..401a8e268d33 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -462,8 +462,10 @@ LIBBPF_1.8.0 {
bpf_program__attach_tracing_multi;
bpf_program__clear_flags;
bpf_program__clone;
+ bpf_program__load_manually;
bpf_program__load_strategy;
bpf_program__set_load_strategy;
+ bpf_program__unload_manually;
btf__find_by_name_kind_own;
btf__new_empty_opts;
} LIBBPF_1.7.0;
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH bpf-next v3 3/6] libbpf: Support declarative manual load via SEC("!...") prefix
2026-09-17 20:30 [PATCH bpf-next v3 0/6] libbpf: BPF program manual loading Andrey Grodzovsky
2026-09-17 20:30 ` [PATCH bpf-next v3 1/6] libbpf: BPF program load strategy enum Andrey Grodzovsky
2026-09-17 20:30 ` [PATCH bpf-next v3 2/6] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
@ 2026-09-17 20:30 ` Andrey Grodzovsky
2026-09-17 21:34 ` bot+bpf-ci
2026-09-17 20:30 ` [PATCH bpf-next v3 4/6] libbpf: Reject gen_loader for objects with already-manual programs Andrey Grodzovsky
` (2 subsequent siblings)
5 siblings, 1 reply; 16+ messages in thread
From: Andrey Grodzovsky @ 2026-09-17 20:30 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source
Add a SEC("!...") section-name prefix, letting a program declare
itself manually-loaded in its source instead of requiring an
imperative bpf_program__set_load_strategy() call, following the exact
convention already used for SEC("?...").
The prefix is recognized and stripped in bpf_object__init_prog(),
before find_sec_def() ever runs, directly setting load_strategy to
MANUAL and initializing autoattach/saved_autoattach - the same place
and same style '?' already uses, and before BTF or sec_def exist.
Assisted-by: Claude:claude-sonnet-5
Suggested-by: Andrii Nakryiko <andrii@kernel.org>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index acb8caffa442..c9a184d9626d 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -872,16 +872,25 @@ bpf_object__init_prog(struct bpf_object *obj, struct bpf_program *prog,
/* libbpf's convention for SEC("?abc...") is that it's just like
* SEC("abc...") but the corresponding bpf_program starts out with
* autoload set to false.
+ *
+ * Similarly, SEC("!abc...") marks the program for manual loading:
+ * it is skipped by the bulk auto-load pass and must be explicitly
+ * loaded later via bpf_program__load_manually().
*/
if (sec_name[0] == '?') {
prog->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;
/* from now on forget there was ? in section name */
sec_name++;
+ } else if (sec_name[0] == '!') {
+ prog->load_strategy = BPF_PROG_LOAD_STRATEGY_MANUAL;
+ /* from now on forget there was ! in section name */
+ sec_name++;
} else {
prog->load_strategy = BPF_PROG_LOAD_STRATEGY_AUTO;
}
- prog->autoattach = true;
+ prog->saved_autoattach = true;
+ prog->autoattach = prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL;
/* inherit object's log_level */
prog->log_level = obj->log_level;
@@ -15287,7 +15296,7 @@ int bpf_program__set_load_strategy(struct bpf_program *prog, enum bpf_prog_load_
* A gen_loader implementation is being called for autoloaded
* programs and defines its own model for loading BPF programs.
* To pass a BPF program to gen_loader, set the program's load strategy
- * to LD_AUTOLOAD.
+ * to BPF_PROG_LOAD_STRATEGY_AUTO.
*/
if (obj->gen_loader)
return libbpf_err(-EOPNOTSUPP);
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH bpf-next v3 4/6] libbpf: Reject gen_loader for objects with already-manual programs
2026-09-17 20:30 [PATCH bpf-next v3 0/6] libbpf: BPF program manual loading Andrey Grodzovsky
` (2 preceding siblings ...)
2026-09-17 20:30 ` [PATCH bpf-next v3 3/6] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
@ 2026-09-17 20:30 ` Andrey Grodzovsky
2026-09-17 21:16 ` bot+bpf-ci
2026-09-17 20:30 ` [PATCH bpf-next v3 5/6] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
2026-09-17 20:30 ` [PATCH bpf-next v3 6/6] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
5 siblings, 1 reply; 16+ messages in thread
From: Andrey Grodzovsky @ 2026-09-17 20:30 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source
bpf_program__set_load_strategy()'s MANUAL case rejects setting MANUAL
strategy while a gen_loader is already attached, but a program marked
MANUAL declaratively (SEC("!...")) gets that strategy during
bpf_object__open(), before bpf_object__gen_loader() can ever be
called, so the existing guard can never observe it.
Left unchecked, bpf_object_load_progs() skips such programs, so
gen->nr_progs undercounts relative to the object's real program
count. bpf_gen__finish() only rejects the opposite mismatch direction
(nr_progs < gen->nr_progs), so this passes silently, and every
generated skeleton program slot after the manual one ends up wired to
the wrong prog_fd.
Catch it at the one point guaranteed to run after any MANUAL marking
has already happened: reject in bpf_object__gen_loader() itself if any
program already has load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL.
Assisted-by: Claude:claude-sonnet-5
Suggested-by: Andrii Nakryiko <andrii@kernel.org>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index c9a184d9626d..3ac7319ed0f4 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -9752,11 +9752,29 @@ int bpf_object__set_kversion(struct bpf_object *obj, __u32 kern_version)
int bpf_object__gen_loader(struct bpf_object *obj, struct gen_loader_opts *opts)
{
struct bpf_gen *gen;
+ size_t i;
if (!opts)
return libbpf_err(-EFAULT);
if (!OPTS_VALID(opts, gen_loader_opts))
return libbpf_err(-EINVAL);
+
+ /*
+ * Manually-loaded programs are not visible to gen_loader (see
+ * bpf_program__set_load_strategy()'s MANUAL case), and marking a
+ * program MANUAL happens during bpf_object__open(), before this
+ * function can ever run, so that guard can never catch it here.
+ * Reject any pre-existing MANUAL program now, since this is the
+ * earliest point where both are known.
+ */
+ for (i = 0; i < obj->nr_programs; i++) {
+ if (obj->programs[i].load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {
+ pr_warn("prog '%s': gen_loader does not support manually-loaded programs\n",
+ obj->programs[i].name);
+ return libbpf_err(-EOPNOTSUPP);
+ }
+ }
+
gen = calloc(1, sizeof(*gen));
if (!gen)
return libbpf_err(-ENOMEM);
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH bpf-next v3 5/6] selftests/bpf: Cover BPF program load strategy transitions
2026-09-17 20:30 [PATCH bpf-next v3 0/6] libbpf: BPF program manual loading Andrey Grodzovsky
` (3 preceding siblings ...)
2026-09-17 20:30 ` [PATCH bpf-next v3 4/6] libbpf: Reject gen_loader for objects with already-manual programs Andrey Grodzovsky
@ 2026-09-17 20:30 ` Andrey Grodzovsky
2026-09-17 20:39 ` sashiko-bot
2026-09-17 21:34 ` bot+bpf-ci
2026-09-17 20:30 ` [PATCH bpf-next v3 6/6] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
5 siblings, 2 replies; 16+ messages in thread
From: Andrey Grodzovsky @ 2026-09-17 20:30 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source
From: Slava Imameev <slava.imameev@crowdstrike.com>
Add load_type test covering load strategy transitions
(DISABLED/AUTO/MANUAL), set_autoload() bool/enum compatibility,
autoattach restore on leaving MANUAL, and the manual load/attach
cycle after the object is loaded.
Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
.../selftests/bpf/prog_tests/load_type.c | 157 ++++++++++++++++++
.../selftests/bpf/progs/test_load_type.c | 31 ++++
2 files changed, 188 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/load_type.c
create mode 100644 tools/testing/selftests/bpf/progs/test_load_type.c
diff --git a/tools/testing/selftests/bpf/prog_tests/load_type.c b/tools/testing/selftests/bpf/prog_tests/load_type.c
new file mode 100644
index 000000000000..55ca53685863
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/load_type.c
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <test_progs.h>
+#include <time.h>
+#include "test_load_type.skel.h"
+
+void test_load_type(void)
+{
+ struct bpf_link *link;
+ struct test_load_type *skel;
+ int err;
+
+ skel = test_load_type__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ /* don't load prog1 */
+ bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);
+
+ /* load and attach prog2 */
+ bpf_program__set_load_strategy(skel->progs.prog2, BPF_PROG_LOAD_STRATEGY_AUTO);
+ if (!ASSERT_TRUE(bpf_program__autoload(skel->progs.prog2), "prog2_autoload"))
+ goto cleanup;
+
+ err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
+ if (!ASSERT_OK(err, "set_load_strategy_manual"))
+ goto cleanup;
+ if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3), BPF_PROG_LOAD_STRATEGY_MANUAL,
+ "prog3_load_strategy"))
+ goto cleanup;
+
+ /*
+ * bpf_program__set_autoload() is a thin forwarder to
+ * set_load_strategy(), restricted to AUTO/DISABLED to preserve its
+ * original bool on/off meaning; it does change the load strategy of
+ * a program that isn't currently BPF_PROG_LOAD_STRATEGY_AUTO.
+ */
+ err = bpf_program__set_autoload(skel->progs.prog3, false);
+ if (!ASSERT_OK(err, "set_autoload_false"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3),
+ BPF_PROG_LOAD_STRATEGY_DISABLED, "prog3_load_strategy_after_false"))
+ goto cleanup;
+
+ err = bpf_program__set_autoload(skel->progs.prog3, true);
+ if (!ASSERT_OK(err, "set_autoload_true"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3), BPF_PROG_LOAD_STRATEGY_AUTO,
+ "prog3_load_strategy_after_true"))
+ goto cleanup;
+
+ err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
+ if (!ASSERT_OK(err, "set_load_strategy_manual_enum"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3), BPF_PROG_LOAD_STRATEGY_MANUAL,
+ "prog3_load_strategy_after_manual_enum"))
+ goto cleanup;
+
+ /*
+ * leaving MANUAL for AUTO must restore autoattach (regression test for
+ * the autoattach residue bug: set_load_strategy(MANUAL) clears autoattach, and
+ * nothing used to restore it on exit)
+ */
+ err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_AUTO);
+ if (!ASSERT_OK(err, "set_load_strategy_auto"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3), BPF_PROG_LOAD_STRATEGY_AUTO,
+ "prog3_load_strategy_auto"))
+ goto cleanup;
+
+ if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog3), "prog3_autoattach_restored"))
+ goto cleanup;
+
+ /*
+ * confirm the restore also holds across a MANUAL -> DISABLED -> AUTO
+ * round-trip: AUTO and DISABLED share one guard keyed off the source
+ * strategy being MANUAL, so autoattach is already restored at the
+ * MANUAL -> DISABLED step, not by a separate DISABLED -> AUTO one
+ */
+ err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
+ if (!ASSERT_OK(err, "set_load_strategy_manual_again"))
+ goto cleanup;
+
+ err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ if (!ASSERT_OK(err, "set_load_strategy_disabled"))
+ goto cleanup;
+
+ err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_AUTO);
+ if (!ASSERT_OK(err, "set_load_strategy_auto_via_disabled"))
+ goto cleanup;
+
+ if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog3),
+ "prog3_autoattach_restored_via_disabled"))
+ goto cleanup;
+
+ /* an out-of-range load strategy is rejected */
+ err = bpf_program__set_load_strategy(skel->progs.prog3, (enum bpf_prog_load_strategy)999);
+ if (!ASSERT_ERR(err, "set_load_strategy_invalid"))
+ goto cleanup;
+
+ /* change the strategy back to BPF_PROG_LOAD_STRATEGY_MANUAL for the rest of the test */
+ err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
+ if (!ASSERT_OK(err, "set_load_strategy_manual_final"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3), BPF_PROG_LOAD_STRATEGY_MANUAL,
+ "prog3_load_strategy_final"))
+ goto cleanup;
+
+ err = test_load_type__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog2), "prog2_autoattach"))
+ goto cleanup;
+ if (!ASSERT_FALSE(bpf_program__autoattach(skel->progs.prog3), "prog3_autoattach"))
+ goto cleanup;
+
+ /* loaded program strategy cannot be changed */
+ err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ ASSERT_ERR(err, "set_load_strategy_after_load");
+
+ err = test_load_type__attach(skel);
+ if (!ASSERT_OK(err, "skel_attach"))
+ goto cleanup;
+
+ 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");
+
+ err = bpf_program__load_manually(skel->progs.prog3);
+ if (!ASSERT_OK(err, "load_manually"))
+ 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_again"))
+ goto cleanup;
+
+ /* detach prog3 as test_load_type__destroy doesn't detach manually loaded programs */
+ err = bpf_link__destroy(link);
+ ASSERT_OK(err, "link_destroy");
+
+cleanup:
+ test_load_type__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/test_load_type.c b/tools/testing/selftests/bpf/progs/test_load_type.c
new file mode 100644
index 000000000000..3d9b81691d7a
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_load_type.c
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+
+bool prog1_called = false;
+bool prog2_called = false;
+bool prog3_called = false;
+
+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;
+}
+
+char _license[] SEC("license") = "GPL";
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH bpf-next v3 6/6] selftests/bpf: Cover BPF program manual loading
2026-09-17 20:30 [PATCH bpf-next v3 0/6] libbpf: BPF program manual loading Andrey Grodzovsky
` (4 preceding siblings ...)
2026-09-17 20:30 ` [PATCH bpf-next v3 5/6] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
@ 2026-09-17 20:30 ` Andrey Grodzovsky
2026-09-17 20:40 ` sashiko-bot
2026-09-17 21:34 ` bot+bpf-ci
5 siblings, 2 replies; 16+ messages in thread
From: Andrey Grodzovsky @ 2026-09-17 20:30 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, martin.kelly, slava.imameev, linux-open-source
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 | 350 ++++++++++++++++++
.../selftests/bpf/prog_tests/signed_loader.c | 28 ++
.../selftests/bpf/progs/test_dynamicload.c | 54 +++
3 files changed, 432 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..979bdc14f4ff
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
@@ -0,0 +1,350 @@
+// 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;
+
+ /* 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.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
+
+ err = test_dynamicload__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ /* prog4 was never loaded, so it cannot be loaded manually either */
+ err = bpf_program__load_manually(skel->progs.prog4);
+ ASSERT_ERR(err, "load_manually_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_manually() 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;
+ 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_manually(skel->progs.prog4);
+ if (!ASSERT_OK(err, "load_manually_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);
+ if (!ASSERT_OK(err, "link_destroy_after_prepare"))
+ goto cleanup;
+
+ err = bpf_program__unload_manually(skel->progs.prog4);
+ ASSERT_OK(err, "unload_manually_after_prepare");
+
+cleanup:
+ 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_manually(skel->progs.prog5);
+ if (!ASSERT_OK(err, "load_manually_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);
+}
+
+void test_dynamicload(void)
+{
+ struct bpf_link *link;
+ 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_manually(skel->progs.prog1);
+ if (!ASSERT_ERR(err, "load_manually_disabled"))
+ goto cleanup;
+
+ /* prog1 is disabled for load */
+ err = bpf_program__unload_manually(skel->progs.prog1);
+ if (!ASSERT_ERR(err, "unload_manually_disabled"))
+ goto cleanup;
+
+ /* prog2 is autoload */
+ err = bpf_program__load_manually(skel->progs.prog2);
+ if (!ASSERT_ERR(err, "load_manually_autoload"))
+ goto cleanup;
+
+ /* prog2 is autoload */
+ err = bpf_program__unload_manually(skel->progs.prog2);
+ if (!ASSERT_ERR(err, "unload_manually_autoload"))
+ goto cleanup;
+
+ /* 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_manually(skel->progs.prog3);
+ if (!ASSERT_OK(err, "load_manually"))
+ 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);
+ 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 */
+ err = bpf_program__unload_manually(skel->progs.prog3);
+ if (!ASSERT_OK(err, "unload_manually"))
+ goto cleanup;
+
+ /* reload prog3 */
+ err = bpf_program__load_manually(skel->progs.prog3);
+ if (!ASSERT_OK(err, "load_manually_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);
+ if (!ASSERT_OK(err, "link_destroy_reattach"))
+ goto cleanup;
+
+ /*
+ * verify regular unload for manually loaded program,
+ * unload prog3 as a regular program
+ */
+ bpf_program__unload(skel->progs.prog3);
+
+ /* reset the call flags after unload */
+ 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");
+
+ /* reloading prog3 must fail as it was unloaded as a regular program */
+ err = bpf_program__load_manually(skel->progs.prog3);
+ ASSERT_ERR(err, "load_manually_after_regular_unload");
+
+ /*
+ * run prog4 (declaratively marked) through the same manual
+ * load/attach/trigger/detach/unload cycle as prog3
+ */
+ err = bpf_program__load_manually(skel->progs.prog4);
+ if (!ASSERT_OK(err, "prog4_load_manually"))
+ 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);
+ if (!ASSERT_OK(err, "prog4_link_destroy"))
+ goto cleanup;
+
+ err = bpf_program__unload_manually(skel->progs.prog4);
+ ASSERT_OK(err, "prog4_unload_manually");
+
+ test_dynamicload__destroy(skel);
+
+ /* separate scenario: imperative override of the declarative marker */
+ dynamicload_verify_override();
+
+ /* separate scenario: prepare()-only is sufficient for manual load */
+ dynamicload_verify_prepare_only();
+
+ /* separate scenario: deferred manual load with a module BTF attach target */
+ dynamicload_verify_module_btf();
+ return;
+
+cleanup:
+ test_dynamicload__destroy(skel);
+}
+
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..156c90afe73e
--- /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,
+ 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
^ permalink raw reply related [flat|nested] 16+ messages in thread
* Re: [PATCH bpf-next v3 5/6] selftests/bpf: Cover BPF program load strategy transitions
2026-09-17 20:30 ` [PATCH bpf-next v3 5/6] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
@ 2026-09-17 20:39 ` sashiko-bot
2026-09-17 21:34 ` bot+bpf-ci
1 sibling, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-09-17 20:39 UTC (permalink / raw)
To: Andrey Grodzovsky; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] bpf: bpf_link leaked on selftest error path
--
commit 82f598b63abc74f2a315872de7cd207f7e441280
Author: Slava Imameev <slava.imameev@crowdstrike.com>
selftests/bpf: Cover BPF program load strategy transitions
This commit adds a load_type test to cover BPF program load strategy
transitions. It verifies transitions between DISABLED, AUTO, and MANUAL
strategies, as well as the behavior of autoattach.
> diff --git a/tools/testing/selftests/bpf/prog_tests/load_type.c b/tools/testing/selftests/bpf/prog_tests/load_type.c
> new file mode 100644
> index 0000000000000..55ca53685863c
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/load_type.c
> @@ -0,0 +1,157 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +#include <test_progs.h>
> +#include <time.h>
> +#include "test_load_type.skel.h"
> +
> +void test_load_type(void)
> +{
> + struct bpf_link *link;
> + struct test_load_type *skel;
[ ... ]
> + /* 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_again"))
> + goto cleanup;
[Severity: Medium]
Will this leak the bpf_link if the assertion fails?
When the test assertion fails, it jumps to the cleanup block in
test_load_type(), skipping the explicit bpf_link__destroy() call. Because
test_load_type__destroy() doesn't clean up manually attached programs,
the link will be leaked.
Also, if you choose to fix this by adding bpf_link__destroy() in the
cleanup block, it looks like the link variable is uninitialized at declaration,
so it might be necessary to initialize it to NULL as well.
> +
> + /* detach prog3 as test_load_type__destroy doesn't detach manually loaded programs */
> + err = bpf_link__destroy(link);
> + ASSERT_OK(err, "link_destroy");
> +
> +cleanup:
> + test_load_type__destroy(skel);
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260917203040.3150212-1-andrey.grodzovsky@crowdstrike.com?part=5
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH bpf-next v3 6/6] selftests/bpf: Cover BPF program manual loading
2026-09-17 20:30 ` [PATCH bpf-next v3 6/6] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
@ 2026-09-17 20:40 ` sashiko-bot
2026-09-17 21:34 ` bot+bpf-ci
1 sibling, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-09-17 20:40 UTC (permalink / raw)
To: Andrey Grodzovsky; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests/bpf: bpf_link leak on assertion error paths in dynamicload
--
commit 0fd345ea8ecacebc3bb75a7415c7f51a2637fd25
Author: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
selftests/bpf: Cover BPF program manual loading
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.
> 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 0000000000000..979bdc14f4ffd
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
[ ... ]
> +static void dynamicload_verify_prepare_only(void)
> +{
> + struct test_dynamicload *skel;
> + struct bpf_link *link;
> + int err;
[ ... ]
> + 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;
[Severity: Medium]
If this assertion fails, we jump to cleanup which calls
test_dynamicload__destroy(skel). Because test_dynamicload__destroy()
doesn't detach manually loaded programs, does this leak the bpf_link
and its file descriptor by bypassing bpf_link__destroy(link)?
> +
> + err = bpf_link__destroy(link);
> + if (!ASSERT_OK(err, "link_destroy_after_prepare"))
> + goto cleanup;
> +
> + err = bpf_program__unload_manually(skel->progs.prog4);
> + ASSERT_OK(err, "unload_manually_after_prepare");
> +
> +cleanup:
> + test_dynamicload__destroy(skel);
> +}
[ ... ]
> +void test_dynamicload(void)
> +{
> + struct bpf_link *link;
> + struct test_dynamicload *skel;
> + int err;
[ ... ]
> + /* load prog3 */
> + err = bpf_program__load_manually(skel->progs.prog3);
> + if (!ASSERT_OK(err, "load_manually"))
> + 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;
[Severity: Medium]
Similar to the issue above, if this assertion fails, we jump directly to
cleanup and bypass bpf_link__destroy(link). Could this leak the link resource
for prog3?
> +
> + /* detach prog3 as test_dynamicload__destroy doesn't detach manually loaded programs */
> + err = bpf_link__destroy(link);
> + if (!ASSERT_OK(err, "link_destroy"))
> + 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;
[Severity: Medium]
Does this error path also leak the reattached bpf_link when jumping
to cleanup?
> +
> + /* detach prog3 as test_dynamicload__destroy doesn't detach manually loaded programs */
> + err = bpf_link__destroy(link);
> + if (!ASSERT_OK(err, "link_destroy_reattach"))
> + goto cleanup;
[ ... ]
> + /*
> + * run prog4 (declaratively marked) through the same manual
> + * load/attach/trigger/detach/unload cycle as prog3
> + */
> + err = bpf_program__load_manually(skel->progs.prog4);
> + if (!ASSERT_OK(err, "prog4_load_manually"))
> + 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;
[Severity: Medium]
Can jumping to cleanup here leave the prog4 bpf_link leaked as well?
> +
> + err = bpf_link__destroy(link);
> + if (!ASSERT_OK(err, "prog4_link_destroy"))
> + goto cleanup;
> +
> + err = bpf_program__unload_manually(skel->progs.prog4);
> + ASSERT_OK(err, "prog4_unload_manually");
> +
> + test_dynamicload__destroy(skel);
> +
> + /* separate scenario: imperative override of the declarative marker */
> + dynamicload_verify_override();
> +
> + /* separate scenario: prepare()-only is sufficient for manual load */
> + dynamicload_verify_prepare_only();
> +
> + /* separate scenario: deferred manual load with a module BTF attach target */
> + dynamicload_verify_module_btf();
> + return;
> +
> +cleanup:
> + test_dynamicload__destroy(skel);
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260917203040.3150212-1-andrey.grodzovsky@crowdstrike.com?part=6
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH bpf-next v3 2/6] libbpf: BPF programs manual loading and attaching
2026-09-17 20:30 ` [PATCH bpf-next v3 2/6] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
@ 2026-09-17 20:41 ` sashiko-bot
2026-09-17 21:34 ` bot+bpf-ci
1 sibling, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-09-17 20:41 UTC (permalink / raw)
To: Andrey Grodzovsky; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] bpf: raw error code logged instead of error string
--
commit 41645c9f0ca68e1802db1c78d41589ed30fa75e4
Author: Slava Imameev <slava.imameev@crowdstrike.com>
libbpf: BPF programs manual loading and attaching
This commit introduces a manual load strategy for BPF programs. It permits
programs to be loaded and attached independently after the initial bpf_object
loading, which skips the auto-load pass to improve performance for utilities
with hundreds of programs.
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index bd510a4e4941c..acb8caffa442e 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -15247,3 +15310,49 @@ enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program
> {
> return prog->load_strategy;
> }
> +
> +/*
> + * This function must be called after bpf_object__prepare (or
> + * bpf_object__load, which calls bpf_object__prepare internally).
> + * Manually-loaded program data is initialized on object prepare.
> + * Post-prepare initialization is not supported.
> + */
> +int
> +bpf_program__load_manually(struct bpf_program *prog)
> +{
> + int err;
> + struct bpf_object *obj = prog->obj;
> +
> + if (obj->state < OBJ_PREPARED)
> + return libbpf_err(-EINVAL);
> +
> + if (prog_is_subprog(obj, prog) || prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)
> + return libbpf_err(-EINVAL);
> +
> + if (prog->fd >= 0)
> + return libbpf_err(-EBUSY);
> +
> + err = bpf_object_load_prog(obj, prog, prog->insns, prog->insns_cnt,
> + obj->license, obj->kern_version, &prog->fd);
> + if (err) {
> + pr_warn("prog '%s': failed to load manually: %d\n", prog->name, err);
[Severity: Low]
Does this code log the raw integer error code instead of a descriptive error
string? When bpf_program__load_manually is called and bpf_object_load_prog
returns an error, this will print the %d format specifier. Other libbpf
load functions typically use %s with errstr(err) to improve debuggability.
> + return libbpf_err(err);
> + }
> +
> + return 0;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260917203040.3150212-1-andrey.grodzovsky@crowdstrike.com?part=2
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH bpf-next v3 4/6] libbpf: Reject gen_loader for objects with already-manual programs
2026-09-17 20:30 ` [PATCH bpf-next v3 4/6] libbpf: Reject gen_loader for objects with already-manual programs Andrey Grodzovsky
@ 2026-09-17 21:16 ` bot+bpf-ci
0 siblings, 0 replies; 16+ messages in thread
From: bot+bpf-ci @ 2026-09-17 21:16 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: ast, martin.kelly, slava.imameev, linux-open-source, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 4079 bytes --]
> libbpf: Reject gen_loader for objects with already-manual programs
>
> bpf_program__set_load_strategy()'s MANUAL case rejects setting MANUAL
> strategy while a gen_loader is already attached, but a program marked
> MANUAL declaratively (SEC("!...")) gets that strategy during
> bpf_object__open(), before bpf_object__gen_loader() can ever be
> called, so the existing guard can never observe it.
>
> Left unchecked, bpf_object_load_progs() skips such programs, so
> gen->nr_progs undercounts relative to the object's real program
> count. bpf_gen__finish() only rejects the opposite mismatch direction
> (nr_progs < gen->nr_progs), so this passes silently, and every
> generated skeleton program slot after the manual one ends up wired to
> the wrong prog_fd.
Since 17ddfa90 ("libbpf: Support declarative manual load via SEC("!...")
prefix") and 41c79ac0 ("libbpf: BPF programs manual loading and
attaching") are in this same series, would it read better to fold this
check into the commit that adds SEC("!...") rather than fixing it up two
patches later?
Also, this isn't a bug, but should the changelog say
bpf_object__load_progs() (two underscores)? The tree has
bpf_object__load_progs() at tools/lib/bpf/libbpf.c:8405.
> Catch it at the one point guaranteed to run after any MANUAL marking
> has already happened: reject in bpf_object__gen_loader() itself if any
> program already has load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL.
>
> Assisted-by: Claude:claude-sonnet-5
> Suggested-by: Andrii Nakryiko <andrii@kernel.org>
> Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
This looks like a bug fix for SEC("!...") declarative manual load support.
Should this include:
Fixes: 17ddfa90d052 ("libbpf: Support declarative manual load via SEC("!...") prefix")
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index c9a184d9626db..3ac7319ed0f40 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
> @@ -9752,11 +9752,29 @@ int bpf_object__gen_loader(struct bpf_object *obj, struct gen_loader_opts *opts)
> int bpf_object__gen_loader(struct bpf_object *obj, struct gen_loader_opts *opts)
> {
> struct bpf_gen *gen;
> + size_t i;
>
> if (!opts)
> return libbpf_err(-EFAULT);
> if (!OPTS_VALID(opts, gen_loader_opts))
> return libbpf_err(-EINVAL);
> +
> + /*
> + * Manually-loaded programs are not visible to gen_loader (see
> + * bpf_program__set_load_strategy()'s MANUAL case), and marking a
> + * program MANUAL happens during bpf_object__open(), before this
> + * function can ever run, so that guard can never catch it here.
> + * Reject any pre-existing MANUAL program now, since this is the
> + * earliest point where both are known.
> + */
This isn't a bug, but could this be trimmed to a line or two pointing at
bpf_program__set_load_strategy()? The 11-line comment already added in
bpf_program__set_load_strategy() explains the same gen_loader/MANUAL
incompatibility. Also, phrasing it to clarify only the declarative
SEC("!...") marking happens during open might be clearer, since the
imperative bpf_program__set_load_strategy(MANUAL) path marks programs
after open.
> + for (i = 0; i < obj->nr_programs; i++) {
> + if (obj->programs[i].load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {
> + pr_warn("prog '%s': gen_loader does not support manually-loaded programs\n",
> + obj->programs[i].name);
> + return libbpf_err(-EOPNOTSUPP);
> + }
> + }
This isn't a bug, but would binding `struct bpf_program *prog =
&obj->programs[i];` at the top of the loop match the way the other
program loops in this file are written? bpf_object__load_progs() and
bpf_object_prepare_progs() both bind a local first rather than
indexing obj->programs[i] repeatedly.
> +
> gen = calloc(1, sizeof(*gen));
> if (!gen)
> return libbpf_err(-ENOMEM);
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35272462686
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH bpf-next v3 1/6] libbpf: BPF program load strategy enum
2026-09-17 20:30 ` [PATCH bpf-next v3 1/6] libbpf: BPF program load strategy enum Andrey Grodzovsky
@ 2026-09-17 21:16 ` bot+bpf-ci
0 siblings, 0 replies; 16+ messages in thread
From: bot+bpf-ci @ 2026-09-17 21:16 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: ast, martin.kelly, slava.imameev, linux-open-source, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1323 bytes --]
> libbpf: BPF program load strategy enum
>
> Replacing the boolean field with an enum simplifies the addition
> of new load strategies. Currently, the bpf_program structure defines
> the autoload behavior using a boolean field. This field is now
> replaced with an enum, allowing new BPF program loading strategies
> to be introduced by extending the enum value range.
This isn't a bug, but would it help to mention in the changelog that
this exports two new API symbols in LIBBPF_1.8.0 and makes
bpf_program__set_autoload() a wrapper, and to name the strategy the
enum is being prepared for?
The changelog describes the internal field swap twice but does not
mention that the patch adds bpf_program__set_load_strategy() and
bpf_program__load_strategy() as new permanent public ABI symbols to
LIBBPF_1.8.0, and turns the existing bpf_program__set_autoload() into
a wrapper over the new setter.
It also does not say which new strategy is coming, which is the
rationale a reviewer needs in order to judge whether a new exported
enum API is warranted over the existing boolean one.
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35272462686
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH bpf-next v3 3/6] libbpf: Support declarative manual load via SEC("!...") prefix
2026-09-17 20:30 ` [PATCH bpf-next v3 3/6] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
@ 2026-09-17 21:34 ` bot+bpf-ci
0 siblings, 0 replies; 16+ messages in thread
From: bot+bpf-ci @ 2026-09-17 21:34 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: ast, martin.kelly, slava.imameev, linux-open-source, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3211 bytes --]
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index acb8caffa442e..c9a184d9626db 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
> @@ -872,16 +872,25 @@ bpf_object__init_prog(struct bpf_object *obj, struct bpf_program *prog,
> /* libbpf's convention for SEC("?abc...") is that it's just like
> * SEC("abc...") but the corresponding bpf_program starts out with
> * autoload set to false.
> + *
> + * Similarly, SEC("!abc...") marks the program for manual loading:
> + * it is skipped by the bulk auto-load pass and must be explicitly
> + * loaded later via bpf_program__load_manually().
> */
> if (sec_name[0] == '?') {
> prog->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;
> /* from now on forget there was ? in section name */
> sec_name++;
> + } else if (sec_name[0] == '!') {
> + prog->load_strategy = BPF_PROG_LOAD_STRATEGY_MANUAL;
> + /* from now on forget there was ! in section name */
> + sec_name++;
> } else {
> prog->load_strategy = BPF_PROG_LOAD_STRATEGY_AUTO;
> }
The declarative SEC("!...") path sets BPF_PROG_LOAD_STRATEGY_MANUAL here in
bpf_object__init_prog(), which runs during bpf_object__open(). But the
imperative API enforces a gen_loader guard when switching to MANUAL:
tools/lib/bpf/libbpf.c:bpf_program__set_load_strategy() {
case BPF_PROG_LOAD_STRATEGY_MANUAL:
/*
* Manually-loaded programs are not supported for gen_loader.
* This is because bpf_object_load_prog is not called for
* manually-loaded programs, so such programs are not visible
* to gen_loader. ...
*/
if (obj->gen_loader)
return libbpf_err(-EOPNOTSUPP);
}
The ordering makes that guard unreachable for SEC("!prog"): this code
runs before obj->gen_loader is installed by bpf_object__gen_loader().
Can this sequence occur?
SEC("!prog")
bpf_object__open()
-> bpf_object__init_prog() sets MANUAL
bpf_object__gen_loader() installs gen_loader
bpf_object__load()
-> skips prog (pr_debug), undercounts gen->nr_progs
-> bpf_gen__finish() wires wrong FDs into skeleton
bpf_object__gen_loader() does not check whether any programs are already
MANUAL:
tools/lib/bpf/libbpf.c:bpf_object__gen_loader() {
...
if (!OPTS_VALID(opts, gen_loader_opts))
return libbpf_err(-EINVAL);
gen = calloc(1, sizeof(*gen));
...
obj->gen_loader = gen;
return 0;
}
So a MANUAL program set via SEC("!...") is silently accepted with
gen_loader, while the same program marked via the imperative API would be
rejected.
The commit message in commit 2ebb83ef300359043b537ca514069942624cc2b8
("libbpf: Reject gen_loader for objects with already-manual programs")
later in the series documents exactly this failure mode. Should that guard
be part of this commit to avoid introducing and then fixing the same issue
within the series?
[ ... ]
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35272462686
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH bpf-next v3 5/6] selftests/bpf: Cover BPF program load strategy transitions
2026-09-17 20:30 ` [PATCH bpf-next v3 5/6] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
2026-09-17 20:39 ` sashiko-bot
@ 2026-09-17 21:34 ` bot+bpf-ci
1 sibling, 0 replies; 16+ messages in thread
From: bot+bpf-ci @ 2026-09-17 21:34 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: ast, martin.kelly, slava.imameev, linux-open-source, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 4946 bytes --]
> diff --git a/tools/testing/selftests/bpf/prog_tests/load_type.c b/tools/testing/selftests/bpf/prog_tests/load_type.c
> new file mode 100644
> index 0000000000000..55ca53685863c
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/load_type.c
[ ... ]
> + /*
> + * leaving MANUAL for AUTO must restore autoattach (regression test for
> + * the autoattach residue bug: set_load_strategy(MANUAL) clears autoattach, and
> + * nothing used to restore it on exit)
> + */
> + err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_AUTO);
> + if (!ASSERT_OK(err, "set_load_strategy_auto"))
> + goto cleanup;
> +
> + if (!ASSERT_EQ(bpf_program__load_strategy(skel->progs.prog3), BPF_PROG_LOAD_STRATEGY_AUTO,
> + "prog3_load_strategy_auto"))
> + goto cleanup;
> +
> + if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog3), "prog3_autoattach_restored"))
> + goto cleanup;
Can this test actually distinguish a correct implementation from a
broken one?
The comment advertises this as a regression test for autoattach-restore
behaviour, but prog3 enters MANUAL with autoattach already true (from
bpf_object__init_prog() initialization), and the test never changes
prog3's autoattach to false before the MANUAL transition.
The libbpf implementation restores from a saved slot:
tools/lib/bpf/libbpf.c:bpf_program__set_load_strategy() {
case BPF_PROG_LOAD_STRATEGY_DISABLED:
case BPF_PROG_LOAD_STRATEGY_AUTO:
if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
prog->autoattach = prog->saved_autoattach;
prog->load_strategy = strategy;
break;
}
Since saved_autoattach is always true for prog3, the ASSERT_TRUE check
would also pass if the restore arm were the broken assignment
"prog->autoattach = true;" instead of the correct
"prog->autoattach = prog->saved_autoattach;".
The discriminating case would be:
err = bpf_program__set_autoattach(skel->progs.prog3, false);
/* ... transition through MANUAL and back to AUTO ... */
ASSERT_FALSE(bpf_program__autoattach(skel->progs.prog3),
"prog3_autoattach_restored_false");
Without that arm, does the test check what its comment claims?
> + /*
> + * confirm the restore also holds across a MANUAL -> DISABLED -> AUTO
> + * round-trip: AUTO and DISABLED share one guard keyed off the source
> + * strategy being MANUAL, so autoattach is already restored at the
> + * MANUAL -> DISABLED step, not by a separate DISABLED -> AUTO one
> + */
> + err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_MANUAL);
> + if (!ASSERT_OK(err, "set_load_strategy_manual_again"))
> + goto cleanup;
> +
> + err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
> + if (!ASSERT_OK(err, "set_load_strategy_disabled"))
> + goto cleanup;
> +
> + err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_AUTO);
> + if (!ASSERT_OK(err, "set_load_strategy_auto_via_disabled"))
> + goto cleanup;
> +
> + if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog3),
> + "prog3_autoattach_restored_via_disabled"))
> + goto cleanup;
The MANUAL -> DISABLED -> AUTO round trip has the same property: it
re-checks a value that is true on every path.
[ ... ]
---
Regarding maintainer feedback from v1 (January 2025):
Andrii Nakryiko raised design concerns about adding the third autoload
state. From the v1 thread
(https://lore.kernel.org/bpf/CAEf4Bzajxh4xvg-aCaBhLQdNOZdhwceYUD2UsCcWku4ZBca_Hw@mail.gmail.com/):
> In general, I'm not too excited about these complications, it's
> error-prone enough with just normal autoload (true/false) logic and
> various interactions between different features. Adding the third
> 'may be loaded much later' state just doesn't seem worth the
> complexity.
He suggested two alternatives: (1) breaking apart BPF objects with
shared maps using bpf_map__reuse_fd() or pinning, or (2) using a
prepare/load split API. In a follow-up message
(https://lore.kernel.org/bpf/CAEf4BzZ=-r7AkSGhru_NMxPcXDLmkVpTiQrBxUvsgq-LE0Lk6w@mail.gmail.com/):
> [B]pf_object won't become multi-threaded and dynamic program
> loading/unloading/loading again cannot be justified. Best proposal
> is to use libbpf for ELF handling and relocations, then take over
> program lifetime outside of bpf_object
While the v3 series (September 2026) reorganized patches and renamed
DYNAMIC to MANUAL, it still implements the third state that Andrii
expressed concerns about. The v3 series has received no human reviews
yet. Has there been further discussion that resolved the maintainer's
design concerns, or is the current approach still open for discussion?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35272462686
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH bpf-next v3 2/6] libbpf: BPF programs manual loading and attaching
2026-09-17 20:30 ` [PATCH bpf-next v3 2/6] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
2026-09-17 20:41 ` sashiko-bot
@ 2026-09-17 21:34 ` bot+bpf-ci
1 sibling, 0 replies; 16+ messages in thread
From: bot+bpf-ci @ 2026-09-17 21:34 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: ast, martin.kelly, slava.imameev, linux-open-source, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 14949 bytes --]
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index bd510a4e4941c..acb8caffa442e 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -8058,7 +8060,7 @@ static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog
> log_buf = prog->log_buf;
> log_buf_size = prog->log_size;
> own_log_buf = false;
> - } else if (obj->log_buf) {
> + } else if (obj->log_buf && prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL) {
> log_buf = obj->log_buf;
> log_buf_size = obj->log_size;
> own_log_buf = false;
Does manually-loaded programs need to skip the object-level verifier log
buffer? obj->log_buf comes from bpf_object_open_opts.kernel_log_buf; it is
caller-owned memory that libbpf never frees, so there doesn't appear to be
a lifetime reason to skip it after the object has been loaded.
The observable effect is that when a user opens an object with
kernel_log_buf set and then loads a program via bpf_program__load_manually(),
this falls through to the else arm, realloc()s its own buffer, and on
failure dumps the log to pr_warn()/pr_debug() instead of into the caller's
buffer, which stays empty. That's the only load path where kernel_log_buf
now does nothing.
[ ... ]
> @@ -9063,33 +9068,50 @@ static void bpf_object_unpin(struct bpf_object *obj)
> bpf_map__unpin(&obj->maps[i], NULL);
> }
>
> -static void bpf_object_cleanup_btf(struct bpf_object *obj)
> +static void bpf_object_cleanup_btf(struct bpf_object *obj, bool force)
> {
> int i;
>
> - /* clean up module BTFs */
> - for (i = 0; i < obj->btf_module_cnt; i++) {
> - close(obj->btf_modules[i].fd);
> - btf__free(obj->btf_modules[i].btf);
> - free(obj->btf_modules[i].name);
> + /*
> + * Module BTF fds may still be borrowed (via fd_array,
> + * attach_btf_obj_fd, or baked into relocated instructions) by
> + * programs that have not been manually loaded yet, so defer
> + * closing them in that case to the end of the object lifetime,
> + * unless the caller forces immediate cleanup.
> + */
> + if (force || !obj->has_manual_progs) {
> + for (i = 0; i < obj->btf_module_cnt; i++) {
> + close(obj->btf_modules[i].fd);
> + btf__free(obj->btf_modules[i].btf);
> + free(obj->btf_modules[i].name);
> + }
> + obj->btf_module_cnt = 0;
> + obj->btf_module_cap = 0;
> + obj->btf_modules_loaded = false;
> + zfree(&obj->btf_modules);
> }
> - obj->btf_module_cnt = 0;
> - obj->btf_module_cap = 0;
> - obj->btf_modules_loaded = false;
> - zfree(&obj->btf_modules);
>
> - /* clean up vmlinux BTF */
> - btf__free(obj->btf_vmlinux);
> - obj->btf_vmlinux = NULL;
> + /*
> + * The btf_vmlinux data is needed for manually loaded programs,
> + * so defer freeing it in that case to the end of the object lifetime.
> + */
> + if (force || !obj->has_manual_progs) {
> + btf__free(obj->btf_vmlinux);
> + obj->btf_vmlinux = NULL;
> + }
> }
[ ... ]
> @@ -9817,9 +9838,13 @@ bool bpf_program__autoattach(const struct bpf_program *prog)
> return prog->autoattach;
> }
>
> -void bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach)
> +int bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach)
> {
> + if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
> + return libbpf_err(-EINVAL);
> +
> prog->autoattach = autoattach;
> + return 0;
> }
The return type of bpf_program__set_autoattach() is changed from void to
int, but the symbol keeps its original version node LIBBPF_1.0.0 in
libbpf.map and no compat symbol is added. This creates a runtime ABI
hazard: an application compiled against the new libbpf.h that does
`if (bpf_program__set_autoattach(prog, false)) goto err;` records an
undefined reference to bpf_program__set_autoattach@LIBBPF_1.0.0.
That version requirement is satisfied by any libbpf.so.1 from 1.0 through
1.7, where the function is void and never writes the return register. The
caller then tests an undefined value and can take a spurious error path.
This also affects source compatibility: any existing user code that takes
the address of the API (e.g. `void (*fn)(struct bpf_program *, bool) =
bpf_program__set_autoattach;`) or that redeclares it in a binding layer
now fails to compile.
Documentation/bpf/libbpf/libbpf_naming_convention.rst, "ABI versioning":
"Every time ABI is being changed, e.g. because a new symbol is added or
semantic of existing symbol is changed, ABI version should be bumped." The
semantics of this symbol changed (it can now fail with -EINVAL) and its
return type changed, yet its version node is untouched.
All six in-tree callers discard the return value, so nothing in-tree
changes behaviour; the exposure is entirely to out-of-tree users of the
stable API.
[ ... ]
> @@ -15236,10 +15261,48 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
>
> int bpf_program__set_load_strategy(struct bpf_program *prog, enum bpf_prog_load_strategy strategy)
> {
> - if (prog->obj->state >= OBJ_LOADED)
> + struct bpf_object *obj = prog->obj;
> +
> + if (obj->state >= OBJ_PREPARED)
> + return libbpf_err(-EINVAL);
The state gate is tightened from OBJ_LOADED to OBJ_PREPARED. Since commit
7a9899b0140f4 (the immediately preceding patch in this series)
bpf_program__set_autoload() is a thin wrapper around
bpf_program__set_load_strategy(), this silently regresses the long-standing
public API bpf_program__set_autoload().
Before this commit the gate was `prog->obj->state >= OBJ_LOADED`, so
autoload could be changed anywhere in the bpf_object__prepare() ->
bpf_object__load() window. After this commit any
bpf_program__set_autoload() call on an object that has been through
bpf_object__prepare() returns -EINVAL.
Turning autoload off after prepare (to skip loading a program) is a
legitimate use of that window, which bpf_object__prepare() exists to
expose (libbpf.h:270: "Leaves BPF object in the state ready for program
loading").
The commit message does not mention this behaviour change, and the
kernel-doc for bpf_program__set_load_strategy() in libbpf.h is now wrong
on both counts: "Can only be called before the enclosing bpf_object is
loaded." and "@return 0 on success; negative error code if the object was
already loaded".
No in-tree caller currently orders set_autoload() after prepare
(veristat.c's process_obj() sets autoload before bpf_object__prepare()), so
the breakage lands on out-of-tree libbpf consumers only.
If the tightening is intentional (MANUAL bookkeeping is finalized in
bpf_object_prepare_progs()), should it apply only to the MANUAL case, or
should the header docs and changelog state that set_autoload()/
set_load_strategy() no longer work after prepare?
[ ... ]
> + switch (strategy) {
> + case BPF_PROG_LOAD_STRATEGY_DISABLED:
> + case BPF_PROG_LOAD_STRATEGY_AUTO:
> + if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
> + prog->autoattach = prog->saved_autoattach;
> + prog->load_strategy = strategy;
> + break;
> + case BPF_PROG_LOAD_STRATEGY_MANUAL:
> + /*
> + * Manually-loaded programs are not supported for gen_loader.
> + * This is because bpf_object_load_prog is not called for
> + * manually-loaded programs, so such programs are not visible
> + * to gen_loader. For this reason, prevent calling
> + * bpf_program__set_load_strategy(MANUAL) when gen_loader was
> + * used to generate a BPF object loader.
> + * A gen_loader implementation is being called for autoloaded
> + * programs and defines its own model for loading BPF programs.
> + * To pass a BPF program to gen_loader, set the program's load strategy
> + * to LD_AUTOLOAD.
> + */
> + if (obj->gen_loader)
> + return libbpf_err(-EOPNOTSUPP);
The gen_loader rejection is order-dependent and can be bypassed, so the
combination the code itself declares unsupported still reaches
bpf_object_load(). bpf_object__gen_loader() (a public API) performs no
symmetric check, so the following sequence is accepted at every step:
obj = bpf_object__open_file(...);
/* state == OBJ_OPEN, gen_loader == NULL */
bpf_program__set_load_strategy(prog, BPF_PROG_LOAD_STRATEGY_MANUAL);
/* passes: obj->gen_loader is still NULL */
bpf_object__gen_loader(obj, &opts);
/* no check for existing MANUAL programs */
bpf_object__load(obj);
Once loaded, bpf_object__load_progs() skips the MANUAL program, so
bpf_gen__prog_load() is never called for it and gen->nr_progs (incremented
only in bpf_gen__prog_load()) undercounts obj->nr_programs.
bpf_gen__finish() only rejects the opposite skew:
if (nr_progs < gen->nr_progs || nr_maps != gen->nr_maps) { ... -EFAULT }
so the mismatch passes silently, and bpf_gen__finish()'s wiring loop maps
prog_desc[i] <- stack prog_fd[i] for i in [0, gen->nr_progs), shifting
every light-skeleton program slot after the skipped MANUAL program onto the
wrong prog_fd. The result is a silently mis-wired generated loader rather
than an error.
The in-code comment directly above the guard overstates what it achieves:
it says the check "prevent[s] calling
bpf_program__set_load_strategy(MANUAL) when gen_loader was used", but it
only covers the gen_loader-then-MANUAL ordering, not
MANUAL-then-gen_loader.
Note also that the early-out at line 15269 (`if (strategy ==
prog->load_strategy) return 0;`) sits before the guard, so a repeat MANUAL
call succeeds even when obj->gen_loader is already set.
Commit 2ebb83ef30035 (forward search over the supplied range 41c79ac..HEAD)
adds the missing check inside bpf_object__gen_loader() and describes
exactly this failure mode.
The comment also tells the reader to "set the program's load strategy to
LD_AUTOLOAD", but no identifier named LD_AUTOLOAD exists anywhere in the
tree. The enumerator is BPF_PROG_LOAD_STRATEGY_AUTO (libbpf.h:2110). The
same comment also names the internal helper as "bpf_object_load_prog" when
the function that actually skips manual programs is bpf_object__load_progs()
- bpf_object_load_prog() is the per-program loader and is called for manual
programs, from bpf_program__load_manually(). Both make the only piece of
documentation for this restriction point at the wrong symbols.
[ ... ]
> + if (prog_is_subprog(obj, prog))
> + return libbpf_err(-EINVAL);
> +
> + prog->saved_autoattach = prog->autoattach;
> + prog->load_strategy = BPF_PROG_LOAD_STRATEGY_MANUAL;
> + prog->autoattach = false;
> + break;
> + default:
> + return libbpf_err(-EINVAL);
> + }
The patch introduces a save/restore protocol for prog->autoattach that is
keyed entirely on load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL, but two
pre-existing sites assign prog->load_strategy directly and were not
updated, so they clobber MANUAL and strand the saved value:
- bpf_object_adjust_struct_ops_autoload() (libbpf.c:1168), run from
bpf_object_prepare() before bpf_object_prepare_progs()
- bpf_map__init_kern_struct_ops() (libbpf.c:1249 and 1288),
`st_ops->progs[i]->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;`
bpf_program__set_load_strategy() rejects subprograms and gen_loader objects
for the MANUAL case, but not struct_ops programs. So for a struct_ops
program the user marked MANUAL:
1. set_load_strategy(MANUAL) stashes saved_autoattach and forces
prog->autoattach = false.
2. bpf_object_prepare() overwrites load_strategy with AUTO or DISABLED,
bypassing the `if (prog->load_strategy ==
BPF_PROG_LOAD_STRATEGY_MANUAL) prog->autoattach =
prog->saved_autoattach;` restore that only lives inside
set_load_strategy().
3. prog->autoattach stays false permanently while
bpf_program__load_strategy() reports AUTO, obj->has_manual_progs is
never set (prepare_progs runs after the clobber and sees AUTO/DISABLED),
and a later bpf_program__load_manually() returns -EINVAL.
All of this happens with no warning: the request is silently discarded and
the program's autoattach flag is left inverted relative to what the user
configured.
Should struct_ops programs be rejected in bpf_program__set_load_strategy()
alongside the existing prog_is_subprog() check, or should the two direct
assignments preserve/restore the MANUAL bookkeeping?
> diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
> index cdf300443f19c..5914e5001b972 100644
> --- a/tools/lib/bpf/libbpf.h
> +++ b/tools/lib/bpf/libbpf.h
[ ... ]
> @@ -378,7 +378,7 @@ LIBBPF_API const char *bpf_program__section_name(const struct bpf_program *prog)
> LIBBPF_API bool bpf_program__autoload(const struct bpf_program *prog);
> LIBBPF_API int bpf_program__set_autoload(struct bpf_program *prog, bool autoload);
> LIBBPF_API bool bpf_program__autoattach(const struct bpf_program *prog);
> -LIBBPF_API void bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach);
> +LIBBPF_API int bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach);
[ ... ]
> @@ -2133,6 +2135,9 @@ LIBBPF_API int bpf_program__set_load_strategy(struct bpf_program *prog,
> */
> LIBBPF_API enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program *prog);
>
> +LIBBPF_API int bpf_program__load_manually(struct bpf_program *prog);
> +LIBBPF_API int bpf_program__unload_manually(struct bpf_program *prog);
The two new public APIs are added as bare declarations with no
documentation comment. Documentation/bpf/libbpf/libbpf_naming_convention.rst,
"API documentation convention", requires public APIs to be documented above
the declaration with @brief/@param and, for non-void returns, @return.
Every other API added by this series follows it -
bpf_program__set_load_strategy() (libbpf.h:2116) and
bpf_program__load_strategy() (libbpf.h:2130) both carry full
@brief/@param/@return blocks - so the omission is also inconsistent within
the same header region.
The undocumented contracts are not guessable from the signatures and are
only visible in the implementation:
- load requires the object to be at least OBJ_PREPARED
(bpf_object__prepare()), not fully loaded, else -EINVAL;
- the program's load strategy must be BPF_PROG_LOAD_STRATEGY_MANUAL and it
must not be a subprogram, else -EINVAL;
- re-loading an already-loaded program returns -EBUSY;
- unload only closes the FD and deliberately retains the program image so
it can be reloaded.
Related naming concern: bpf_program__unload_manually() sits alongside the
long-standing public bpf_program__unload() (libbpf.h:462, LIBBPF_0.0.1),
and the two have deliberately different semantics -
bpf_program__unload() also zfree()s prog->func_info, prog->line_info and
prog->subprogs, making the program unreloadable, whereas the new one
preserves them. With neither declaration documented, the near-identical
names give a caller no way to tell them apart.
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35272462686
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH bpf-next v3 6/6] selftests/bpf: Cover BPF program manual loading
2026-09-17 20:30 ` [PATCH bpf-next v3 6/6] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
2026-09-17 20:40 ` sashiko-bot
@ 2026-09-17 21:34 ` bot+bpf-ci
1 sibling, 0 replies; 16+ messages in thread
From: bot+bpf-ci @ 2026-09-17 21:34 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: ast, martin.kelly, slava.imameev, linux-open-source, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 10728 bytes --]
> 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 0000000000000..979bdc14f4ffd
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
> @@ -0,0 +1,350 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +#include <test_progs.h>
> +#include <time.h>
> +#include "test_dynamicload.skel.h"
> +
> +#define READ_SZ 456
[ ... ]
> +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;
> +
> + /* 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.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
Does this comment match the code? progs/test_dynamicload.c declares
prog1, prog2 and prog3 all as SEC("raw_tp/sys_enter"), meaning all three
are autoload programs. Only prog1 and prog3 are disabled here, so prog2
remains enabled and gets loaded by test_dynamicload__load() below.
Compare with dynamicload_verify_module_btf() which disables all three
(prog1, prog2, prog3) after an identical comment. Should prog2 be
disabled here too, or should the comment explain which programs are being
disabled and why prog2 is left enabled?
Also, neither of these two set_load_strategy() return values is checked,
so a future failure to disable prog1 or prog3 would go unnoticed.
> +
> + err = test_dynamicload__load(skel);
> + if (!ASSERT_OK(err, "skel_load"))
> + goto cleanup;
> +
> + /* prog4 was never loaded, so it cannot be loaded manually either */
> + err = bpf_program__load_manually(skel->progs.prog4);
> + ASSERT_ERR(err, "load_manually_after_override");
Is this comment accurate? Looking at bpf_program__load_manually() in
libbpf.c (added by 41c79ac032fd4), it rejects prog4 because its strategy
is BPF_PROG_LOAD_STRATEGY_DISABLED:
if (prog_is_subprog(obj, prog) ||
prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)
return libbpf_err(-EINVAL);
The reason for rejection is the strategy value, not "was never loaded."
In fact, a MANUAL program is also never loaded by the bulk pass, yet
bpf_program__load_manually() succeeds for it - which test_dynamicload()
demonstrates for this exact program at the "prog4_load_manually"
assertion later. Would it be clearer to say that prog4 was overridden to
DISABLED and manual load is only permitted for MANUAL programs?
[ ... ]
> +static void dynamicload_verify_module_btf(void)
> +{
> + struct test_dynamicload *skel;
> + struct bpf_link *link;
> + int err;
> +
> + if (!env.has_testmod) {
> + test__skip();
> + return;
> + }
Does test__skip() work correctly here? This helper runs outside any
test__start_subtest() scope, so test_progs.c skips the *entire*
dynamicload test rather than just the module-BTF scenario.
Looking at test_progs.c test__skip() implementation:
void test__skip(void)
{
if (env.subtest_state)
env.subtest_state->skipped = true;
else
env.test_state->skip_cnt++;
}
dynamicload.c defines no subtests - test_dynamicload() calls these three
helpers as plain function calls - so env.subtest_state is NULL and
test_state->skip_cnt is incremented. print_test_result() then reports:
else if (skipped_cnt == subtests_cnt || !subtests_cnt)
fprintf(env.stdout_saved, "SKIP");
With subtest_num == 0 and skip_cnt == 1, the !subtests_cnt arm fires and
the test prints SKIP. This means on any host without bpf_testmod,
'#N dynamicload' is reported as SKIP even though the main manual
load/attach/reload cycle, the imperative-override scenario, and the
prepare-only scenario all ran and passed.
Every neighbouring testmod-dependent test uses the same test__skip()
snippet from *inside* a subtest helper reached via test__start_subtest()
(module_attach.c, ksyms_module.c, btf_module.c, bpf_cookie.c, iters.c,
tp_btf_ids.c). Wrapping the three scenarios in test__start_subtest()
("override", "prepare_only", "module_btf") would scope the skip correctly
and prevent an early failure in test_dynamicload()'s main body from
skipping all three scenarios via goto cleanup. Would that pattern work
here?
[ ... ]
> +void test_dynamicload(void)
> +{
> + struct bpf_link *link;
> + struct test_dynamicload *skel;
> + int err;
> +
> + skel = test_dynamicload__open();
> + if (!ASSERT_OK_PTR(skel, "skel_open"))
> + return;
[ ... ]
> + /*
> + * verify regular unload for manually loaded program,
> + * unload prog3 as a regular program
> + */
> + bpf_program__unload(skel->progs.prog3);
> +
> + /* reset the call flags after unload */
> + 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");
> +
> + /* reloading prog3 must fail as it was unloaded as a regular program */
> + err = bpf_program__load_manually(skel->progs.prog3);
> + ASSERT_ERR(err, "load_manually_after_regular_unload");
Does bpf_program__load_manually() actually detect that a program was
unloaded with the plain bpf_program__unload() API? Looking at
bpf_program__load_manually() in libbpf.c (added by 41c79ac032fd4), its
only rejections are obj->state < OBJ_PREPARED, prog_is_subprog(),
load_strategy != MANUAL, and prog->fd >= 0.
After bpf_program__unload(), the fd is -1 and prog->insns is still
intact, so the reload is actually attempted and reaches the kernel. It
only fails because bpf_program__unload() (libbpf.c:800) zeros the
pointers:
void bpf_program__unload(struct bpf_program *prog)
{
zclose(prog->fd);
zfree(&prog->func_info);
zfree(&prog->line_info);
zfree(&prog->subprogs);
}
but leaves func_info_cnt / line_info_cnt / *_rec_size set. So
bpf_object_load_prog() passes load_attr.func_info = NULL with
func_info_cnt = 1 and the kernel's check_btf_func() returns -EFAULT.
Two consequences: (a) this assertion is conditional on the func/line-info
triple being submitted - bpf_object_load_prog() only fills it in if
obj->btf && btf__fd(obj->btf) >= 0 && kernel_supports(FEAT_BTF_FUNC) -
so on a build/kernel where object BTF is not loaded the reload succeeds
and this subtest fails; (b) any cleanup of bpf_program__unload() that
also zeroes the stale counts silently flips this from a passing to a
failing test. Should either the contract be stated and enforced in
bpf_program__load_manually(), or this assertion dropped?
[ ... ]
> + test_dynamicload__destroy(skel);
> +
> + /* separate scenario: imperative override of the declarative marker */
> + dynamicload_verify_override();
> +
> + /* separate scenario: prepare()-only is sufficient for manual load */
> + dynamicload_verify_prepare_only();
> +
> + /* separate scenario: deferred manual load with a module BTF attach target */
> + dynamicload_verify_module_btf();
> + return;
> +
> +cleanup:
> + test_dynamicload__destroy(skel);
> +}
Would these scenarios benefit from test__start_subtest() wrappers? The
four scenarios are invoked inline, so the test registers zero subtests.
When dynamicload_verify_module_btf() calls test__skip() because
env.has_testmod is false, test_progs' print_test_result() reports the
entire dynamicload test as SKIP even though the main manual
load/attach/reload scenario plus the override and prepare-only scenarios
all ran and passed.
With subtest_num == 0 and skip_cnt == 1, the !subtests_cnt arm in
print_test_result() fires and the pass is silently converted into a skip.
A failure in one scenario also cannot be attributed to a specific
scenario. Wrapping each scenario in test__start_subtest() would keep the
skip scoped to the module-BTF case and report it as "OK (SKIP: 1/4)".
[ ... ]
> 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 0000000000000..156c90afe73e9
> --- /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;
> +}
A subsystem pattern flags this as potentially concerning: this new BPF
object duplicates progs/test_load_type.c, which the immediately preceding
commit (c7c06b9e606e1, "selftests/bpf: Cover BPF program load strategy
transitions") added in the same series.
The three globals bool prog1_called/prog2_called/prog3_called and the
three SEC("raw_tp/sys_enter") bodies prog1/prog2/prog3 are byte-identical
in the two files. test_dynamicload.c only adds prog4
(SEC("!raw_tp/sys_enter")), prog5 (SEC("?fentry")), prog5_sz and the
extra #include <bpf/bpf_tracing.h>.
Both tests exercise the same new load-strategy API (load_type.c covers
the strategy transitions, dynamicload.c the manual load/attach/reload
cycle), so the series ends up building and shipping two nearly identical
BPF objects and two skeletons for one feature.
The two test files do cover distinct behavior of the new API, and keeping
the manual-load scenarios in their own object avoids one test's
set_load_strategy() calls perturbing the other's. On that reading the
duplication is only the BPF-side scaffolding (about 20 lines) rather than
duplicated test coverage. Still, would it make sense to add prog4/prog5
to progs/test_load_type.c (and the subtests to prog_tests/load_type.c)
instead of adding a second object with a copy of the same three programs?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35272462686
^ permalink raw reply [flat|nested] 16+ messages in thread
end of thread, other threads:[~2026-09-17 21:34 UTC | newest]
Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-17 20:30 [PATCH bpf-next v3 0/6] libbpf: BPF program manual loading Andrey Grodzovsky
2026-09-17 20:30 ` [PATCH bpf-next v3 1/6] libbpf: BPF program load strategy enum Andrey Grodzovsky
2026-09-17 21:16 ` bot+bpf-ci
2026-09-17 20:30 ` [PATCH bpf-next v3 2/6] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
2026-09-17 20:41 ` sashiko-bot
2026-09-17 21:34 ` bot+bpf-ci
2026-09-17 20:30 ` [PATCH bpf-next v3 3/6] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
2026-09-17 21:34 ` bot+bpf-ci
2026-09-17 20:30 ` [PATCH bpf-next v3 4/6] libbpf: Reject gen_loader for objects with already-manual programs Andrey Grodzovsky
2026-09-17 21:16 ` bot+bpf-ci
2026-09-17 20:30 ` [PATCH bpf-next v3 5/6] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
2026-09-17 20:39 ` sashiko-bot
2026-09-17 21:34 ` bot+bpf-ci
2026-09-17 20:30 ` [PATCH bpf-next v3 6/6] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
2026-09-17 20:40 ` sashiko-bot
2026-09-17 21:34 ` bot+bpf-ci
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox