* [PATCH bpf-next v9 0/7] libbpf: BPF program manual loading
@ 2026-10-29 22:33 Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
` (6 more replies)
0 siblings, 7 replies; 10+ messages in thread
From: Andrey Grodzovsky @ 2026-10-29 22:33 UTC (permalink / raw)
To: bpf, andrii, ast; +Cc: martin.kelly, slava.imameev
This series was originally posted in January 2025 [1] as a two-patch RFC
introducing a per-program tri-state load strategy (disabled/auto/manual)
to let large BPF applications load a subset of their programs lazily,
independent of the initial bpf_object load.
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 left vulnerable.
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.
Changes since v8 [2]:
- New Patch 2: free func_info, line_info and subprogs in
bpf_program__exit(), so bpf_program__unload() only closes the fd
for any program (Alexei).
- Patch 4: keep the reloc_desc of MANUAL programs when the bulk load frees
relocation descriptors, so the verifier log of a later
bpf_program__load() still gets the CO-RE relocation fixup (Alexei,
Sashiko)
- Patch 4: comment the force argument of bpf_object_post_load_cleanup()
at the bpf_object__close() call (Andrii)
- Patch 6: merge the load strategy transitions test into the manual load
test as a subtest instead of a standalone load_type test, dropping
the part of it that duplicated the manual load cycle (Sashiko)
- Drop Assisted-by tags from commits, it was recently removed from documentation
in mainline.
Development was assisted by LLM.
[1] https://lore.kernel.org/bpf/20250122215206.59859-1-slava.imameev@crowdstrike.com/
[2] https://lore.kernel.org/bpf/20261024014514.2959514-1-andrey.grodzovsky@crowdstrike.com/
Andrey Grodzovsky (4):
libbpf: Reject gen_loader set after PREPARE step.
libbpf: Free func_info, line_info and subprogs in bpf_program__exit()
libbpf: Support declarative manual load via SEC("!...") prefix
selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL
Slava Imameev (3):
libbpf: BPF program load strategy enum
libbpf: BPF programs manual loading and attaching
selftests/bpf: Cover BPF program load strategies and manual loading
tools/lib/bpf/libbpf.c | 261 +++++++--
tools/lib/bpf/libbpf.h | 84 +++
tools/lib/bpf/libbpf.map | 4 +
.../selftests/bpf/prog_tests/manual_load.c | 507 ++++++++++++++++++
.../selftests/bpf/prog_tests/signed_loader.c | 32 ++
.../bpf/prog_tests/test_struct_ops_arena.c | 48 +-
.../selftests/bpf/progs/test_manual_load.c | 70 +++
tools/testing/selftests/bpf/veristat.c | 20 +-
8 files changed, 968 insertions(+), 58 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/manual_load.c
create mode 100644 tools/testing/selftests/bpf/progs/test_manual_load.c
base-commit: d80373a8e70d3193151232c3062abbbe367ff5ad
--
2.34.1
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH bpf-next v9 1/7] libbpf: Reject gen_loader set after PREPARE step.
2026-10-29 22:33 [PATCH bpf-next v9 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
@ 2026-10-29 22:33 ` Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 2/7] libbpf: Free func_info, line_info and subprogs in bpf_program__exit() Andrey Grodzovsky
` (5 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Andrey Grodzovsky @ 2026-10-29 22:33 UTC (permalink / raw)
To: bpf, andrii, ast; +Cc: martin.kelly, slava.imameev
It's wrong running the loader post prepare because
gen_loader records the BTF load, map creation and extern/CO-RE
relocation steps while the object is being prepared. If it is enabled
after bpf_object__prepare(), those steps are never recorded.
Suggested-by: Andrii Nakryiko <andrii@kernel.org>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 8fcb8cf39cc0..6bb5887090cd 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -10698,6 +10698,9 @@ int bpf_object__gen_loader(struct bpf_object *obj, struct gen_loader_opts *opts)
return libbpf_err(-EFAULT);
if (!OPTS_VALID(opts, gen_loader_opts))
return libbpf_err(-EINVAL);
+ if (obj->state >= OBJ_PREPARED)
+ return libbpf_err(-EINVAL);
+
gen = calloc(1, sizeof(*gen));
if (!gen)
return libbpf_err(-ENOMEM);
--
2.34.1
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH bpf-next v9 2/7] libbpf: Free func_info, line_info and subprogs in bpf_program__exit()
2026-10-29 22:33 [PATCH bpf-next v9 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
@ 2026-10-29 22:33 ` Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 3/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
` (4 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Andrey Grodzovsky @ 2026-10-29 22:33 UTC (permalink / raw)
To: bpf, andrii, ast; +Cc: martin.kelly, slava.imameev
Nothing depends on them being freed in bpf_program__unload. Free
them in bpf_program__exit() together with the rest of the
per-program data instead. This is a clenaup step in preparation
for manual program loading work that follows.
Suggested-by: Alexei Starovoitov <alexei.starovoitov@gmail.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 6bb5887090cd..ff04718d47c1 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -876,10 +876,6 @@ void bpf_program__unload(struct bpf_program *prog)
return;
zclose(prog->fd);
-
- zfree(&prog->func_info);
- zfree(&prog->line_info);
- zfree(&prog->subprogs);
}
static void bpf_program__exit(struct bpf_program *prog)
@@ -888,6 +884,9 @@ static void bpf_program__exit(struct bpf_program *prog)
return;
bpf_program__unload(prog);
+ zfree(&prog->func_info);
+ zfree(&prog->line_info);
+ zfree(&prog->subprogs);
zfree(&prog->name);
zfree(&prog->sec_name);
zfree(&prog->insns);
--
2.34.1
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH bpf-next v9 3/7] libbpf: BPF program load strategy enum
2026-10-29 22:33 [PATCH bpf-next v9 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 2/7] libbpf: Free func_info, line_info and subprogs in bpf_program__exit() Andrey Grodzovsky
@ 2026-10-29 22:33 ` Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 4/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
` (3 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Andrey Grodzovsky @ 2026-10-29 22:33 UTC (permalink / raw)
To: bpf, andrii, ast; +Cc: martin.kelly, slava.imameev
From: Slava Imameev <slava.imameev@crowdstrike.com>
This commit is in preparation for the next commit that introduces
manual program loading. 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.
New getter and setter API is also declared for this enum handling.
Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 53 +++++++++++++++++++++++++---------------
tools/lib/bpf/libbpf.h | 33 +++++++++++++++++++++++++
tools/lib/bpf/libbpf.map | 3 +++
3 files changed, 69 insertions(+), 20 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index ff04718d47c1..e2cea1bca1d2 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;
/* the program or a function that it calls has callx */
@@ -956,11 +956,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;
@@ -1247,7 +1247,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;
@@ -1338,7 +1339,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;
}
@@ -1376,7 +1377,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;
@@ -3811,7 +3812,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;
@@ -6500,7 +6501,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
@@ -8171,7 +8172,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);
@@ -8207,7 +8208,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 */
@@ -9299,8 +9300,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;
@@ -10092,7 +10093,8 @@ static int bpf_object_load(struct bpf_object *obj, int extra_log_level, const ch
prog_has_callx(&obj->programs[i]))
text_has_callx = true;
for (i = 0; i < obj->nr_programs; i++)
- if (obj->programs[i].autoload && !prog_is_subprog(obj, &obj->programs[i]) &&
+ if (obj->programs[i].load_strategy != BPF_PROG_LOAD_STRATEGY_DISABLED &&
+ !prog_is_subprog(obj, &obj->programs[i]) &&
(text_has_callx || prog_has_callx(&obj->programs[i])))
nr_progs++;
bpf_gen__init(obj->gen_loader, obj->log_level | extra_log_level,
@@ -10776,16 +10778,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)
@@ -16174,7 +16173,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 */
@@ -16284,3 +16283,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_PREPARED)
+ 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 03f794f4adb0..a91e43cbba20 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -2117,6 +2117,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 prepared.
+ *
+ * @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 prepared
+ */
+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 eeba4072a5da..c06fc6b68efd 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -475,4 +475,7 @@ LIBBPF_1.8.0 {
} LIBBPF_1.7.0;
LIBBPF_1.9.0 {
+ global:
+ bpf_program__load_strategy;
+ bpf_program__set_load_strategy;
} LIBBPF_1.8.0;
--
2.34.1
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH bpf-next v9 4/7] libbpf: BPF programs manual loading and attaching
2026-10-29 22:33 [PATCH bpf-next v9 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (2 preceding siblings ...)
2026-10-29 22:33 ` [PATCH bpf-next v9 3/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
@ 2026-10-29 22:33 ` Andrey Grodzovsky
2026-10-29 22:54 ` sashiko-bot
2026-10-29 22:33 ` [PATCH bpf-next v9 5/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
` (2 subsequent siblings)
6 siblings, 1 reply; 10+ messages in thread
From: Andrey Grodzovsky @ 2026-10-29 22:33 UTC (permalink / raw)
To: bpf, andrii, ast; +Cc: martin.kelly, slava.imameev
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()
and unloaded via bpf_program__unload(). It only requires that
bpf object is already in PREPARE state (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.
A few key changes described below:
gen_loader is rejected if any manual program is present since
this loader is finalized during bpf_object__load()
and so it will miss any manual programs loaded later.
Two program log related APIs were relaxed to work post OBJ_LOADED
state to fix regression where verifier logs were lost in case of
bpf_object__prepare failure. It makes sense to relax those for MANUAL
programs as they can load post OBJ_LOADED state so the log buff still
can be set at this point. Also add proper documentation to the setters.
Failures with BPF_PROG_TYPE_STRUCT_OPS were encountered and so in several
places code related to struct_ops logic was altered to take into account
struct_ops program with MANUAL strategy set. The general idea is to reject
this type of programs for manual loading since those programs cannot
load/unload independently of each other per a given struct ops.
Hence, SEC("!...") style declaration is rejected for them while
bpf_program__set_load_strategy() does accept them to allow certain
flexibility in the API future usage.
Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Co-developed-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 194 ++++++++++++++++++++++++++++++++++-----
tools/lib/bpf/libbpf.h | 51 ++++++++++
tools/lib/bpf/libbpf.map | 1 +
3 files changed, 225 insertions(+), 21 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index e2cea1bca1d2..1a5b582d63b2 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -758,6 +758,7 @@ struct bpf_object {
bool has_subcalls;
bool has_rodata;
+ bool has_manual_progs;
struct bpf_gen *gen_loader;
@@ -1227,6 +1228,14 @@ static int bpf_object_adjust_struct_ops_autoload(struct bpf_object *obj)
if (prog->type != BPF_PROG_TYPE_STRUCT_OPS)
continue;
+ /*
+ * If the user set the load strategy to MANUAL, don't guess it
+ * from map autocreate: a MANUAL program is loaded explicitly
+ * with bpf_program__load(), which rejects any other strategy.
+ */
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
+ continue;
+
for (j = 0; j < obj->nr_maps; ++j) {
const struct btf_type *type;
@@ -5873,6 +5882,20 @@ static int init_prog_array_slots(struct bpf_object *obj, struct bpf_map *map)
continue;
targ_prog = map->init_slots[i];
+
+ /*
+ * Be careful about manual progs, reject if they weren't loaded before
+ * this step to not insert invalid FDs into the map, otherwise we will
+ * get a much less clear rejection from the kernel on the element update
+ * below.
+ */
+ if (targ_prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL &&
+ targ_prog->fd < 0) {
+ pr_warn("map '%s': slot [%u] prog '%s' is MANUAL but not loaded\n",
+ map->name, i, targ_prog->name);
+ return -EINVAL;
+ }
+
fd = bpf_program__fd(targ_prog);
err = bpf_map_update_elem(map->fd, &i, &fd, 0);
@@ -7591,6 +7614,14 @@ bpf_object__free_relocs(struct bpf_object *obj)
/* free up relocation descriptors */
for (i = 0; i < obj->nr_programs; i++) {
prog = &obj->programs[i];
+
+ /*
+ * skip MANUAL programs: they might be loading post obj load and
+ * need the reloc_desc to fix up the verifier log on a failed
+ * CO-RE relocation.
+ */
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
+ continue;
zfree(&prog->reloc_desc);
prog->nr_reloc = 0;
}
@@ -9329,6 +9360,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;
@@ -9355,6 +9388,18 @@ static int bpf_object_init_progs(struct bpf_object *obj, const struct bpf_object
prog->type = prog->sec_def->prog_type;
prog->expected_attach_type = prog->sec_def->expected_attach_type;
+ /*
+ * struct_ops programs declared SEC("!...") are rejected here
+ * at declarative parse time for manual loading since those programs cannot
+ * load/unload independently of each other per a given struct ops.
+ */
+ if (prog->type == BPF_PROG_TYPE_STRUCT_OPS &&
+ prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {
+ pr_warn("prog '%s': struct_ops programs do not support manual loading\n",
+ prog->name);
+ return -EINVAL;
+ }
+
/* sec_def can have custom callback which should be called
* after bpf_program is initialized to adjust its properties
*/
@@ -9946,7 +9991,7 @@ static int bpf_object__resolve_externs(struct bpf_object *obj,
return 0;
}
-static void bpf_map_prepare_vdata(const struct bpf_map *map)
+static int bpf_map_prepare_vdata(const struct bpf_map *map)
{
const struct btf_type *type;
struct bpf_struct_ops *st_ops;
@@ -9962,16 +10007,32 @@ static void bpf_map_prepare_vdata(const struct bpf_map *map)
if (!prog)
continue;
+ /*
+ * Manual struct_ops programs don't get along well with manual
+ * loading: all the progs belonging to the same struct_ops must
+ * load and unload together, which is the opposite of the manual
+ * load concept.
+ * Still, we allow manual progs here as long as they were loaded
+ * before this step because this way they do work.
+ */
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL && prog->fd < 0) {
+ pr_warn("map '%s': struct_ops member prog '%s' is MANUAL but not loaded\n",
+ map->name, prog->name);
+ return -EINVAL;
+ }
+
prog_fd = bpf_program__fd(prog);
kern_data = st_ops->kern_vdata + st_ops->kern_func_off[i];
*(unsigned long *)kern_data = prog_fd;
}
+
+ return 0;
}
static int bpf_object_prepare_struct_ops(struct bpf_object *obj)
{
struct bpf_map *map;
- int i;
+ int i, err;
for (i = 0; i < obj->nr_maps; i++) {
map = &obj->maps[i];
@@ -9982,7 +10043,9 @@ static int bpf_object_prepare_struct_ops(struct bpf_object *obj)
if (!map->autocreate)
continue;
- bpf_map_prepare_vdata(map);
+ err = bpf_map_prepare_vdata(map);
+ if (err)
+ return err;
}
return 0;
@@ -9998,10 +10061,19 @@ 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;
+ /*
+ * Module BTF fds and btf_vmlinux are still needed by
+ * programs that have not been manually loaded yet, so defer freeing
+ * them to the end of the object lifetime, unless the caller forces
+ * immediate cleanup.
+ */
+ if (!force && obj->has_manual_progs)
+ return;
+
/* clean up module BTFs */
for (i = 0; i < obj->btf_module_cnt; i++) {
close(obj->btf_modules[i].fd);
@@ -10018,15 +10090,42 @@ static void bpf_object_cleanup_btf(struct bpf_object *obj)
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, and the slots handed out with it */
- zfree(&obj->fd_array);
- obj->fd_array_cap = 0;
- obj->fd_array_used = 0;
+ /*
+ * The fd array, and the slots handed out with it, are needed for
+ * manually loaded programs, so defer freeing them in that case
+ * to the end of the object lifetime.
+ */
+ if (force || !obj->has_manual_progs) {
+ zfree(&obj->fd_array);
+ obj->fd_array_cap = 0;
+ obj->fd_array_used = 0;
+ }
/* clean up BTF */
- bpf_object_cleanup_btf(obj);
+ bpf_object_cleanup_btf(obj, force);
+}
+
+static int bpf_object_check_gen_loader(struct bpf_object *obj)
+{
+ struct bpf_program *prog;
+
+ if (!obj->gen_loader)
+ return 0;
+
+ /*
+ * gen_loader only records programs loaded by bpf_object__load_progs(),
+ * which skips MANUAL ones.
+ */
+ bpf_object__for_each_program(prog, obj) {
+ if (prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)
+ continue;
+ pr_warn("prog '%s': gen_loader does not support manually-loaded programs\n",
+ prog->name);
+ return -EOPNOTSUPP;
+ }
+ return 0;
}
static int bpf_object_prepare(struct bpf_object *obj, const char *target_btf_path)
@@ -10038,7 +10137,8 @@ static int bpf_object_prepare(struct bpf_object *obj, const char *target_btf_pat
return -EINVAL;
}
- err = bpf_object_prepare_token(obj);
+ err = bpf_object_check_gen_loader(obj);
+ err = err ? : bpf_object_prepare_token(obj);
err = err ? : bpf_object__probe_loading(obj);
err = err ? : bpf_object__load_vmlinux_btf(obj, false);
err = err ? : bpf_object__resolve_externs(obj, obj->kconfig);
@@ -10121,7 +10221,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) {
@@ -10586,10 +10686,11 @@ void bpf_object__close(struct bpf_object *obj)
/*
* if user called bpf_object__prepare() without ever getting to
- * bpf_object__load(), we need to clean up stuff that is normally
- * cleaned up at the end of loading step
+ * bpf_object__load() or we have manual programs,
+ * we need to clean up stuff that is otherwise cleaned up at
+ * the end of loading step
*/
- bpf_object_post_load_cleanup(obj);
+ bpf_object_post_load_cleanup(obj, true /* force */);
usdt_manager_free(obj->usdt_man);
obj->usdt_man = NULL;
@@ -10598,7 +10699,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++)
@@ -10924,7 +11024,13 @@ __u32 bpf_program__log_level(const struct bpf_program *prog)
int bpf_program__set_log_level(struct bpf_program *prog, __u32 log_level)
{
- if (prog->obj->state >= OBJ_LOADED)
+ /*
+ * Here and below, OBJ_LOADED is not a relevant restriction for
+ * manual programs as those can load after the bpf object is
+ * already loaded.
+ */
+ if (prog->obj->state >= OBJ_LOADED &&
+ prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)
return libbpf_err(-EBUSY);
prog->log_level = log_level;
@@ -10943,7 +11049,8 @@ int bpf_program__set_log_buf(struct bpf_program *prog, char *log_buf, size_t log
return libbpf_err(-EINVAL);
if (log_size > UINT_MAX)
return libbpf_err(-EINVAL);
- if (prog->obj->state >= OBJ_LOADED)
+ if (prog->obj->state >= OBJ_LOADED &&
+ prog->load_strategy != BPF_PROG_LOAD_STRATEGY_MANUAL)
return libbpf_err(-EBUSY);
prog->log_buf = log_buf;
@@ -13729,7 +13836,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;
}
@@ -16286,10 +16393,26 @@ 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_PREPARED)
+ struct bpf_object *obj = prog->obj;
+
+ if (obj->state >= OBJ_PREPARED)
+ return libbpf_err(-EINVAL);
+
+ switch (strategy) {
+ case BPF_PROG_LOAD_STRATEGY_DISABLED:
+ case BPF_PROG_LOAD_STRATEGY_AUTO:
+ prog->load_strategy = strategy;
+ break;
+ case BPF_PROG_LOAD_STRATEGY_MANUAL:
+ if (prog_is_subprog(obj, prog))
+ return libbpf_err(-EINVAL);
+
+ prog->load_strategy = BPF_PROG_LOAD_STRATEGY_MANUAL;
+ break;
+ default:
return libbpf_err(-EINVAL);
+ }
- prog->load_strategy = strategy;
return 0;
}
@@ -16297,3 +16420,32 @@ enum bpf_prog_load_strategy bpf_program__load_strategy(const struct bpf_program
{
return prog->load_strategy;
}
+
+int
+bpf_program__load(struct bpf_program *prog)
+{
+ int err;
+ struct bpf_object *obj = prog->obj;
+
+ /*
+ * This function cannot be called before OBJ_PREPARED -
+ * Program's data is initialized on object prepare.
+ */
+ 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: %s\n", prog->name, errstr(err));
+ return libbpf_err(err);
+ }
+
+ return 0;
+}
diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
index a91e43cbba20..22204a544b23 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -473,6 +473,12 @@ LIBBPF_API int bpf_program__pin(struct bpf_program *prog, const char *path);
* @return 0, on success; negative error code, otherwise
*/
LIBBPF_API int bpf_program__unpin(struct bpf_program *prog, const char *path);
+
+/**
+ * @brief **bpf_program__unload()** unloads a BPF program, closing its fd.
+ *
+ * @param prog BPF program to unload
+ */
LIBBPF_API void bpf_program__unload(struct bpf_program *prog);
struct bpf_link;
@@ -1055,8 +1061,39 @@ LIBBPF_API int bpf_program__clear_flags(struct bpf_program *prog, __u32 flags);
* interactions.
*/
LIBBPF_API __u32 bpf_program__log_level(const struct bpf_program *prog);
+
+/**
+ * @brief **bpf_program__set_log_level()** sets the verifier log level used
+ * when the BPF program is loaded.
+ *
+ * Fails with -EBUSY once the enclosing bpf_object is loaded, unless the
+ * program's load strategy is BPF_PROG_LOAD_STRATEGY_MANUAL, as such a program
+ * can still be loaded afterwards with bpf_program__load().
+ *
+ * @param prog BPF program to set the log level for
+ * @param log_level verifier log level, 0 requests no logging on the first
+ * load attempt
+ * @return 0, on success; negative error code, otherwise
+ */
LIBBPF_API int bpf_program__set_log_level(struct bpf_program *prog, __u32 log_level);
LIBBPF_API const char *bpf_program__log_buf(const struct bpf_program *prog, size_t *log_size);
+
+/**
+ * @brief **bpf_program__set_log_buf()** sets the buffer the verifier log of
+ * the BPF program is written to when it is loaded.
+ *
+ * Fails with -EBUSY once the enclosing bpf_object is loaded, unless the
+ * program's load strategy is BPF_PROG_LOAD_STRATEGY_MANUAL, as such a program
+ * can still be loaded afterwards with bpf_program__load().
+ *
+ * @param prog BPF program to set the log buffer for
+ * @param log_buf buffer for the verifier log, or NULL to use the object's
+ * kernel_log_buf
+ * @param log_size size of @log_buf in bytes; must be 0 if @log_buf is NULL and
+ * non-zero otherwise
+ * @return 0, on success; -EINVAL if @log_buf and @log_size are inconsistent or
+ * @log_size exceeds UINT_MAX; -EBUSY as described above
+ */
LIBBPF_API int bpf_program__set_log_buf(struct bpf_program *prog, char *log_buf, size_t log_size);
LIBBPF_API struct bpf_func_info *bpf_program__func_info(const struct bpf_program *prog);
@@ -2122,10 +2159,14 @@ 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.
+ * It is skipped by the skeleton's auto-attach, whatever its autoattach
+ * setting is.
*/
enum bpf_prog_load_strategy {
BPF_PROG_LOAD_STRATEGY_DISABLED = 0,
BPF_PROG_LOAD_STRATEGY_AUTO,
+ BPF_PROG_LOAD_STRATEGY_MANUAL,
};
/**
@@ -2150,6 +2191,16 @@ 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);
+/**
+ * @brief **bpf_program__load()** loads a MANUAL BPF program. The enclosing
+ * bpf_object must already be prepared.
+ *
+ * @param prog BPF program to load; non MANUAL programs, already loaded
+ * programs and subprograms are rejected.
+ * @return 0 on success; negative error code otherwise
+ */
+LIBBPF_API int bpf_program__load(struct bpf_program *prog);
+
#ifdef __cplusplus
} /* extern "C" */
#endif
diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
index c06fc6b68efd..a45666f546fa 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -476,6 +476,7 @@ LIBBPF_1.8.0 {
LIBBPF_1.9.0 {
global:
+ bpf_program__load;
bpf_program__load_strategy;
bpf_program__set_load_strategy;
} LIBBPF_1.8.0;
--
2.34.1
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH bpf-next v9 5/7] libbpf: Support declarative manual load via SEC("!...") prefix
2026-10-29 22:33 [PATCH bpf-next v9 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (3 preceding siblings ...)
2026-10-29 22:33 ` [PATCH bpf-next v9 4/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
@ 2026-10-29 22:33 ` Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 6/7] selftests/bpf: Cover BPF program load strategies and manual loading Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL Andrey Grodzovsky
6 siblings, 0 replies; 10+ messages in thread
From: Andrey Grodzovsky @ 2026-10-29 22:33 UTC (permalink / raw)
To: bpf, andrii, ast; +Cc: martin.kelly, slava.imameev
Add a SEC("!...") section-name prefix, letting a program declare
itself manually loaded in its source following the exact
convention already used for SEC("?...").
Suggested-by: Andrii Nakryiko <andrii@kernel.org>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 1a5b582d63b2..bddc18f8eeff 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -955,11 +955,19 @@ 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 via bpf_program__load().
*/
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;
}
--
2.34.1
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH bpf-next v9 6/7] selftests/bpf: Cover BPF program load strategies and manual loading
2026-10-29 22:33 [PATCH bpf-next v9 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (4 preceding siblings ...)
2026-10-29 22:33 ` [PATCH bpf-next v9 5/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
@ 2026-10-29 22:33 ` Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL Andrey Grodzovsky
6 siblings, 0 replies; 10+ messages in thread
From: Andrey Grodzovsky @ 2026-10-29 22:33 UTC (permalink / raw)
To: bpf, andrii, ast; +Cc: martin.kelly, slava.imameev
From: Slava Imameev <slava.imameev@crowdstrike.com>
Add manual_load tests covering the load strategy transitions
(DISABLED/AUTO/MANUAL), set_autoload() bool compatibility, 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 deferred loads of a module BTF
attach target and of a program calling a module kfunc.
Also add a signed_loader test verifying bpf_object__load() fails
with a gen loader object and a MANUAL program.
Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Co-developed-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
.../selftests/bpf/prog_tests/manual_load.c | 507 ++++++++++++++++++
.../selftests/bpf/prog_tests/signed_loader.c | 32 ++
.../selftests/bpf/progs/test_manual_load.c | 70 +++
3 files changed, 609 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/manual_load.c
create mode 100644 tools/testing/selftests/bpf/progs/test_manual_load.c
diff --git a/tools/testing/selftests/bpf/prog_tests/manual_load.c b/tools/testing/selftests/bpf/prog_tests/manual_load.c
new file mode 100644
index 000000000000..d574f8776916
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/manual_load.c
@@ -0,0 +1,507 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <test_progs.h>
+#include <time.h>
+#include "kfunc_call_test.skel.h"
+#include "test_manual_load.skel.h"
+
+#define READ_SZ 256
+
+/*
+ * Every transition between DISABLED, AUTO and MANUAL, the bool compatibility
+ * of bpf_program__set_autoload(), and the strategy being fixed once the
+ * object is loaded.
+ */
+static void manual_load_verify_transitions(void)
+{
+ struct test_manual_load *skel;
+ int err;
+
+ skel = test_manual_load__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ /* don't load prog1 */
+ err = bpf_program__set_load_strategy(skel->progs.prog1, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ if (!ASSERT_OK(err, "set_load_strategy_disabled_prog1"))
+ goto cleanup;
+
+ /* load and attach prog2 */
+ err = bpf_program__set_load_strategy(skel->progs.prog2, BPF_PROG_LOAD_STRATEGY_AUTO);
+ if (!ASSERT_OK(err, "set_load_strategy_auto_prog2"))
+ goto cleanup;
+ 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;
+ */
+ 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;
+
+ /* changing the load strategy leaves autoattach alone */
+ 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_default"))
+ goto cleanup;
+
+ bpf_program__set_autoattach(skel->progs.prog3, false);
+
+ 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_AUTO);
+ if (!ASSERT_OK(err, "set_load_strategy_auto_again"))
+ goto cleanup;
+
+ if (!ASSERT_FALSE(bpf_program__autoattach(skel->progs.prog3), "prog3_autoattach_kept"))
+ goto cleanup;
+
+ /* restore autoattach to true for the rest of the test */
+ bpf_program__set_autoattach(skel->progs.prog3, true);
+
+ /* 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_EQ(err, -EINVAL, "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_manual_load__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ /* Post load we cannot update a program's strategy */
+ err = bpf_program__set_load_strategy(skel->progs.prog3, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ ASSERT_EQ(err, -EINVAL, "set_load_strategy_after_load");
+
+cleanup:
+ test_manual_load__destroy(skel);
+}
+
+/*
+ * 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 manual_load_verify_override(void)
+{
+ struct test_manual_load *skel;
+ int err;
+
+ skel = test_manual_load__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;
+
+ err = test_manual_load__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ /*
+ * prog4 was overridden to DISABLED, so its load_strategy != MANUAL
+ * and load() must reject it
+ */
+ if (!ASSERT_EQ(bpf_program__fd(skel->progs.prog4), -ENOENT, "prog4_not_loaded"))
+ goto cleanup;
+
+ err = bpf_program__load(skel->progs.prog4);
+ ASSERT_EQ(err, -EINVAL, "load_after_override");
+
+cleanup:
+ test_manual_load__destroy(skel);
+}
+
+/*
+ * prog4 is MANUAL via its SEC("!...") marker; verify that
+ * bpf_object__prepare() alone -- without ever calling bpf_object__load() --
+ * is sufficient for bpf_program__load() to succeed, since BTF
+ * loading, map creation, and relocation of MANUAL programs are all
+ * completed by prepare() already.
+ */
+static void manual_load_verify_prepare_only(void)
+{
+ struct test_manual_load *skel;
+ struct bpf_link *link = NULL;
+ int err;
+
+ skel = test_manual_load__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ skel->bss->test_pid = getpid();
+
+ err = bpf_object__prepare(skel->obj);
+ if (!ASSERT_OK(err, "bpf_object__prepare"))
+ goto cleanup;
+
+ err = bpf_program__load(skel->progs.prog4);
+ if (!ASSERT_OK(err, "load_after_prepare"))
+ goto cleanup;
+
+ if (!ASSERT_GE(bpf_program__fd(skel->progs.prog4), 0, "prog4_fd_after_prepare"))
+ goto cleanup;
+
+ link = bpf_program__attach(skel->progs.prog4);
+ if (!ASSERT_OK_PTR(link, "attach_after_prepare"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog4_called, "prog4_called_after_prepare"))
+ goto cleanup;
+
+ err = bpf_link__destroy(link);
+ link = NULL;
+ if (!ASSERT_OK(err, "link_destroy_after_prepare"))
+ goto cleanup;
+
+ bpf_program__unload(skel->progs.prog4);
+ ASSERT_LT(bpf_program__fd(skel->progs.prog4), 0, "prog4_fd_closed_after_unload");
+
+cleanup:
+ if (link)
+ bpf_link__destroy(link);
+ test_manual_load__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.
+ */
+static void manual_load_verify_module_btf(void)
+{
+ struct test_manual_load *skel;
+ struct bpf_link *link;
+ int err;
+
+ if (!env.has_testmod) {
+ test__skip();
+ return;
+ }
+
+ skel = test_manual_load__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ skel->bss->test_pid = getpid();
+
+ 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_manual_load__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ /*
+ * deferred load must still succeed: the module BTF fd cached above
+ * by set_attach_target() must still be a valid, open fd here
+ */
+ err = bpf_program__load(skel->progs.prog5);
+ if (!ASSERT_OK(err, "load_module_btf"))
+ goto cleanup;
+
+ link = bpf_program__attach(skel->progs.prog5);
+ if (!ASSERT_OK_PTR(link, "attach"))
+ goto cleanup;
+
+ ASSERT_OK(trigger_module_test_read(READ_SZ), "trigger_read");
+ ASSERT_EQ(skel->bss->prog5_sz, READ_SZ, "prog5_sz");
+
+ bpf_link__destroy(link);
+
+cleanup:
+ test_manual_load__destroy(skel);
+}
+
+static void manual_load_verify_main_cycle(void)
+{
+ struct bpf_link *link = NULL;
+ struct test_manual_load *skel;
+ int err;
+
+ skel = test_manual_load__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ skel->bss->test_pid = getpid();
+
+ /*
+ * 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;
+
+ /* 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_manual_load__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ err = test_manual_load__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 */
+ if (!ASSERT_EQ(bpf_program__fd(skel->progs.prog1), -ENOENT, "prog1_not_loaded"))
+ goto cleanup;
+
+ err = bpf_program__load(skel->progs.prog1);
+ if (!ASSERT_EQ(err, -EINVAL, "load_disabled"))
+ goto cleanup;
+
+ /* prog2 is autoload */
+ err = bpf_program__load(skel->progs.prog2);
+ if (!ASSERT_EQ(err, -EINVAL, "load_autoload"))
+ goto cleanup;
+
+ bpf_program__unload(skel->progs.prog2);
+ ASSERT_LT(bpf_program__fd(skel->progs.prog2), 0, "prog2_fd_closed_after_unload");
+
+ /* load prog3 */
+ err = bpf_program__load(skel->progs.prog3);
+ if (!ASSERT_OK(err, "load"))
+ goto cleanup;
+
+ /* attach prog3 */
+ link = bpf_program__attach(skel->progs.prog3);
+ if (!ASSERT_OK_PTR(link, "attach"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called"))
+ goto cleanup;
+
+ /*
+ * detach prog3 explicitly: its link is kept in a local variable, not in
+ * skel->links, so test_manual_load__destroy() has nothing to detach
+ */
+ err = bpf_link__destroy(link);
+ link = NULL;
+ if (!ASSERT_OK(err, "link_destroy"))
+ goto cleanup;
+
+ /* reset the call flag after detach */
+ skel->bss->prog3_called = false;
+
+ usleep(1);
+
+ ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+ /* unload prog3; only its fd is closed, so it can be loaded again below */
+ bpf_program__unload(skel->progs.prog3);
+
+ /* reload prog3 */
+ err = bpf_program__load(skel->progs.prog3);
+ if (!ASSERT_OK(err, "load_reload"))
+ goto cleanup;
+
+ /* reattach prog3 */
+ link = bpf_program__attach(skel->progs.prog3);
+ if (!ASSERT_OK_PTR(link, "reattach"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called_reattach"))
+ goto cleanup;
+
+ err = bpf_link__destroy(link);
+ link = NULL;
+ if (!ASSERT_OK(err, "link_destroy_reattach"))
+ goto cleanup;
+
+ /* reset the call flag after detach */
+ skel->bss->prog3_called = false;
+
+ usleep(1);
+
+ ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+ /*
+ * run prog4 (declaratively marked) through the same manual
+ * load/attach/trigger/detach/unload cycle as prog3
+ */
+ err = bpf_program__load(skel->progs.prog4);
+ if (!ASSERT_OK(err, "prog4_load"))
+ goto cleanup;
+
+ link = bpf_program__attach(skel->progs.prog4);
+ if (!ASSERT_OK_PTR(link, "prog4_attach"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog4_called, "prog4_called"))
+ goto cleanup;
+
+ err = bpf_link__destroy(link);
+ link = NULL;
+ if (!ASSERT_OK(err, "prog4_link_destroy"))
+ goto cleanup;
+
+ bpf_program__unload(skel->progs.prog4);
+
+ test_manual_load__destroy(skel);
+ return;
+
+cleanup:
+ if (link)
+ bpf_link__destroy(link);
+ test_manual_load__destroy(skel);
+}
+
+/*
+ * A MANUAL program that calls a kfunc of a module: the module BTF fd sits in
+ * a slot of the fd_array of the object, which has to survive the bulk
+ * bpf_object__load() for the deferred bpf_program__load() to work.
+ */
+static void manual_load_verify_module_kfunc(void)
+{
+ struct bpf_program *prog, *p;
+ struct kfunc_call_test *skel;
+ int err;
+
+ if (!env.has_testmod) {
+ test__skip();
+ return;
+ }
+
+ skel = kfunc_call_test__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ prog = skel->progs.kfunc_call_test1;
+ err = bpf_program__set_load_strategy(prog, BPF_PROG_LOAD_STRATEGY_MANUAL);
+ if (!ASSERT_OK(err, "set_load_strategy"))
+ goto out;
+
+ /* keep the other programs out of the way of this load */
+ bpf_object__for_each_program(p, skel->obj) {
+ if (p != prog)
+ bpf_program__set_load_strategy(p, BPF_PROG_LOAD_STRATEGY_DISABLED);
+ }
+
+ /* the bulk load skips the program, but this is where the fd_array could go */
+ err = kfunc_call_test__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto out;
+ if (!ASSERT_LT(bpf_program__fd(prog), 0, "not_loaded"))
+ goto out;
+
+ err = bpf_program__load(prog);
+ if (ASSERT_OK(err, "load")) {
+ ASSERT_GE(bpf_program__fd(prog), 0, "prog_fd");
+ bpf_program__unload(prog);
+ ASSERT_LT(bpf_program__fd(prog), 0, "unloaded");
+ }
+out:
+ kfunc_call_test__destroy(skel);
+}
+
+void test_manual_load(void)
+{
+ if (test__start_subtest("transitions"))
+ manual_load_verify_transitions();
+
+ if (test__start_subtest("main_cycle"))
+ manual_load_verify_main_cycle();
+
+ if (test__start_subtest("verify_override"))
+ manual_load_verify_override();
+
+ if (test__start_subtest("verify_prepare_only"))
+ manual_load_verify_prepare_only();
+
+ if (test__start_subtest("verify_module_btf"))
+ manual_load_verify_module_btf();
+
+ if (test__start_subtest("verify_module_kfunc"))
+ manual_load_verify_module_kfunc();
+}
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index e43dba8fed8e..8005cba5bf05 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -2213,6 +2213,37 @@ static void signed_module_kfunc_rejected(void)
run_setup("cleanup", dir);
}
+/*
+ * gen_loader only records programs loaded by bpf_object__load_progs(), which
+ * skips MANUAL ones, so an object with a MANUAL program must fail to load
+ * instead of 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);
+ if (!ASSERT_OK(err, "gen_loader"))
+ goto cleanup;
+
+ err = bpf_object__load(skel->obj);
+ ASSERT_EQ(err, -EOPNOTSUPP, "load_rejected");
+
+cleanup:
+ test_signed_loader__destroy(skel);
+}
+
enum subtest_boot {
BOOT_ANY,
BOOT_SEALED,
@@ -2257,6 +2288,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_manual_load.c b/tools/testing/selftests/bpf/progs/test_manual_load.c
new file mode 100644
index 000000000000..d91d5267a0ed
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_manual_load.c
@@ -0,0 +1,70 @@
+// 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;
+__u32 test_pid = 0;
+
+SEC("raw_tp/sys_enter")
+int prog1(const void *ctx)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != test_pid)
+ return 0;
+
+ prog1_called = true;
+ return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog2(const void *ctx)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != test_pid)
+ return 0;
+
+ prog2_called = true;
+ return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog3(const void *ctx)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != test_pid)
+ return 0;
+
+ prog3_called = true;
+ return 0;
+}
+
+SEC("!raw_tp/sys_enter")
+int prog4(const void *ctx)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != test_pid)
+ return 0;
+
+ prog4_called = true;
+ return 0;
+}
+
+/*
+ * disabled at parse time; its attach target is resolved against module
+ * BTF via bpf_program__set_attach_target() before it is switched to
+ * MANUAL and loaded
+ */
+SEC("?fentry")
+int BPF_PROG(prog5, struct file *file, struct kobject *kobj,
+ const struct bin_attribute *bin_attr, char *buf, loff_t off, size_t len)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != test_pid)
+ return 0;
+
+ prog5_sz = len;
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
--
2.34.1
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH bpf-next v9 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL
2026-10-29 22:33 [PATCH bpf-next v9 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (5 preceding siblings ...)
2026-10-29 22:33 ` [PATCH bpf-next v9 6/7] selftests/bpf: Cover BPF program load strategies and manual loading Andrey Grodzovsky
@ 2026-10-29 22:33 ` Andrey Grodzovsky
2026-10-29 22:45 ` sashiko-bot
6 siblings, 1 reply; 10+ messages in thread
From: Andrey Grodzovsky @ 2026-10-29 22:33 UTC (permalink / raw)
To: bpf, andrii, ast; +Cc: martin.kelly, slava.imameev
veristat used bpf_program__clone() to verify each program in
isolation. Replace it with BPF_PROG_LOAD_STRATEGY_MANUAL plus
bpf_program__load()/bpf_program__unload().
Unlike clone(), which skips binding RODATA maps to avoid mutating
shared object state, bpf_program__load() does bind them - but each
program is unloaded right after verification, dropping the binding
along with it.
Also, add relevant test case to struct_ops_arena tests.
Suggested-by: Andrii Nakryiko <andrii@kernel.org>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
.../bpf/prog_tests/test_struct_ops_arena.c | 48 +++++++++++++++++--
tools/testing/selftests/bpf/veristat.c | 20 ++++----
2 files changed, 53 insertions(+), 15 deletions(-)
diff --git a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
index e7580b9f0fc2..a8bd10bab5dd 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
@@ -192,9 +192,9 @@ static void arena_autobind(void)
}
/*
- * The same binding through bpf_object__prepare() and bpf_program__clone(), the
- * way veristat loads a program: the arena takes slot 0 of a continuous
- * fd_array, next to the module BTF of the kfunc the trigger calls.
+ * The same binding through bpf_object__prepare() and bpf_program__clone(),
+ * the arena takes slot 0 of a continuous fd_array, next to the module BTF
+ * of the kfunc the trigger calls.
*/
static void arena_autobind_clone(void)
{
@@ -225,6 +225,46 @@ static void arena_autobind_clone(void)
struct_ops_arena_autobind__destroy(skel);
}
+/*
+ * The same binding through BPF_PROG_LOAD_STRATEGY_MANUAL: every program is
+ * loaded with bpf_program__load() after bpf_object__prepare() and unloaded
+ * again, the way veristat loads programs.
+ */
+static void arena_autobind_manual(void)
+{
+ struct struct_ops_arena_autobind *skel;
+ struct bpf_map_info info = {};
+ __u32 len = sizeof(info);
+ struct bpf_program *prog;
+ int err;
+
+ skel = struct_ops_arena_autobind__open();
+ if (!ASSERT_OK_PTR(skel, "struct_ops_arena_autobind__open"))
+ return;
+
+ bpf_object__for_each_program(prog, skel->obj) {
+ err = bpf_program__set_load_strategy(prog, BPF_PROG_LOAD_STRATEGY_MANUAL);
+ if (!ASSERT_OK(err, "set_load_strategy"))
+ goto out;
+ }
+ err = bpf_object__prepare(skel->obj);
+ if (!ASSERT_OK(err, "prepare"))
+ goto out;
+ err = bpf_map_get_info_by_fd(bpf_map__fd(skel->maps.arena), &info, &len);
+ if (!ASSERT_OK(err, "arena_info"))
+ goto out;
+ bpf_object__for_each_program(prog, skel->obj) {
+ err = bpf_program__load(prog);
+ if (!ASSERT_OK(err, bpf_program__name(prog)))
+ continue;
+ assert_prog_map(bpf_program__fd(prog), info.id, bpf_program__name(prog));
+ bpf_program__unload(prog);
+ ASSERT_LT(bpf_program__fd(prog), 0, "unloaded");
+ }
+out:
+ struct_ops_arena_autobind__destroy(skel);
+}
+
/*
* An arena exclusive to a program is bound to that program all the same: the
* kernel checks an exclusive map against the digest of the final program, so
@@ -315,6 +355,8 @@ void serial_test_struct_ops_arena(void)
arena_autobind();
if (test__start_subtest("arena_autobind_clone"))
arena_autobind_clone();
+ if (test__start_subtest("arena_autobind_manual"))
+ arena_autobind_manual();
if (test__start_subtest("arena_autobind_excl"))
arena_autobind_excl();
#else
diff --git a/tools/testing/selftests/bpf/veristat.c b/tools/testing/selftests/bpf/veristat.c
index 9cfc9b4b41c1..0cd19b59037f 100644
--- a/tools/testing/selftests/bpf/veristat.c
+++ b/tools/testing/selftests/bpf/veristat.c
@@ -1678,7 +1678,6 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf
const char *base_filename = basename(strdupa(filename));
const char *prog_name = bpf_program__name(prog);
long mem_peak_a, mem_peak_b, mem_peak = -1;
- LIBBPF_OPTS(bpf_prog_load_opts, opts);
char *buf;
int buf_sz, log_level;
struct verif_stats *stats;
@@ -1726,18 +1725,15 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf
if (env.force_reg_invariants)
bpf_program__add_flags(prog, BPF_F_TEST_REG_INVARIANTS);
- opts.log_buf = buf;
- opts.log_size = buf_sz;
- opts.log_level = log_level;
+ bpf_program__set_log_buf(prog, buf, buf_sz);
+ bpf_program__set_log_level(prog, log_level);
cgroup_err = reset_stat_cgroup();
mem_peak_a = cgroup_memory_peak();
- fd = bpf_program__clone(prog, &opts);
- if (fd < 0) {
- err = fd;
- if (env.verbose)
- fprintf(stderr, "Failed to load program %s %d\n", prog_name, err);
- }
+ err = bpf_program__load(prog);
+ fd = err == 0 ? bpf_program__fd(prog) : -1;
+ if (err && env.verbose)
+ fprintf(stderr, "Failed to load program %s %d\n", prog_name, err);
mem_peak_b = cgroup_memory_peak();
if (!cgroup_err && mem_peak_a >= 0 && mem_peak_b >= 0)
mem_peak = mem_peak_b - mem_peak_a;
@@ -1774,7 +1770,7 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf
if (verif_log_buf != buf)
free(buf);
if (fd > 0)
- close(fd);
+ bpf_program__unload(prog);
return 0;
}
@@ -2297,7 +2293,7 @@ static int process_obj(const char *filename)
env.files_processed++;
bpf_object__for_each_program(prog, obj) {
- bpf_program__set_autoload(prog, true);
+ bpf_program__set_load_strategy(prog, BPF_PROG_LOAD_STRATEGY_MANUAL);
prog_cnt++;
}
--
2.34.1
^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [PATCH bpf-next v9 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL
2026-10-29 22:33 ` [PATCH bpf-next v9 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL Andrey Grodzovsky
@ 2026-10-29 22:45 ` sashiko-bot
0 siblings, 0 replies; 10+ messages in thread
From: sashiko-bot @ 2026-10-29 22:45 UTC (permalink / raw)
To: Andrey Grodzovsky, bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests/bpf: uninitialized log buffer in veristat process_prog()
--
commit fdd973586d17be0beb9ecafccb03a9c1c652febc
Author: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL
This patch converts veristat from using bpf_program__clone() to
BPF_PROG_LOAD_STRATEGY_MANUAL with bpf_program__load() and
bpf_program__unload() so that RODATA maps are bound during program
verification. It also adds a test case in test_struct_ops_arena.c covering
manual program loading.
> diff --git a/tools/testing/selftests/bpf/veristat.c b/tools/testing/selftests/bpf/veristat.c
> index 9cfc9b4b41c12..0cd19b59037f6 100644
> --- a/tools/testing/selftests/bpf/veristat.c
> +++ b/tools/testing/selftests/bpf/veristat.c
> @@ -1678,7 +1678,6 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf
> const char *base_filename = basename(strdupa(filename));
> const char *prog_name = bpf_program__name(prog);
> long mem_peak_a, mem_peak_b, mem_peak = -1;
> - LIBBPF_OPTS(bpf_prog_load_opts, opts);
> char *buf;
> int buf_sz, log_level;
> struct verif_stats *stats;
> @@ -1726,18 +1725,15 @@ static int process_prog(const char *filename, struct bpf_object *obj, struct bpf
> if (env.force_reg_invariants)
> bpf_program__add_flags(prog, BPF_F_TEST_REG_INVARIANTS);
>
> - opts.log_buf = buf;
> - opts.log_size = buf_sz;
> - opts.log_level = log_level;
> + bpf_program__set_log_buf(prog, buf, buf_sz);
> + bpf_program__set_log_level(prog, log_level);
>
> cgroup_err = reset_stat_cgroup();
> mem_peak_a = cgroup_memory_peak();
> - fd = bpf_program__clone(prog, &opts);
> - if (fd < 0) {
> - err = fd;
> - if (env.verbose)
> - fprintf(stderr, "Failed to load program %s %d\n", prog_name, err);
> - }
> + err = bpf_program__load(prog);
> + fd = err == 0 ? bpf_program__fd(prog) : -1;
> + if (err && env.verbose)
> + fprintf(stderr, "Failed to load program %s %d\n", prog_name, err);
[Severity: Medium]
Can bpf_program__load(prog) read uninitialized heap memory or over-read buf
when veristat is run with -v or --top-src-lines?
In tools/testing/selftests/bpf/veristat.c:process_prog(), when env.verbose or
env.top_src_lines > 0 is set, buf is allocated with malloc(buf_sz), but only
verif_log_buf[0] is initialized to '\0':
if (env.verbose || env.top_src_lines > 0) {
buf_sz = env.log_size ? env.log_size : max_verifier_log_size();
buf = malloc(buf_sz);
if (!buf)
return -ENOMEM;
...
} else {
buf = verif_log_buf;
buf_sz = sizeof(verif_log_buf);
...
}
verif_log_buf[0] = '\0';
Previously, bpf_program__clone() did not inspect buf in userspace, and
parse_verif_log() later set buf[buf_sz - 1] = '\0' before calling strlen(buf).
With bpf_program__set_log_buf(prog, buf, buf_sz) and bpf_program__load(prog),
if program loading fails before the kernel verifier writes to the log buffer
(for example, an early check in bpf_prog_load() before bpf_check() emits any
verifier log), bpf_program__load() -> bpf_object_load_prog() calls
fixup_verifier_log() in tools/lib/bpf/libbpf.c:
/* post-process verifier log to improve error descriptions */
fixup_verifier_log(prog, log_buf, log_buf_size);
which immediately calls strlen(buf) on the uninitialized heap buffer:
if (!buf)
return;
log_sz = strlen(buf) + 1;
In addition, if bpf_object__prepare() or bpf_program__load() fails without
writing to buf, parse_verif_log(), printf(), and print_top_src_lines() in
process_prog() will read uninitialized or stale log contents from a previous
program's freed buffer, and freeing buf at the end of process_prog() leaves
prog->log_buf pointing to freed memory.
Should process_prog() initialize buf[0] = '\0' (and buf[buf_sz - 1] = '\0')
instead of verif_log_buf[0] = '\0', and clear the log buffer on prog before
freeing buf?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261029223348.3730738-1-andrey.grodzovsky@crowdstrike.com?part=7
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH bpf-next v9 4/7] libbpf: BPF programs manual loading and attaching
2026-10-29 22:33 ` [PATCH bpf-next v9 4/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
@ 2026-10-29 22:54 ` sashiko-bot
0 siblings, 0 replies; 10+ messages in thread
From: sashiko-bot @ 2026-10-29 22:54 UTC (permalink / raw)
To: Andrey Grodzovsky, bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] libbpf: bpf_map__init_kern_struct_ops() clobbers MANUAL load strategy
--
commit dc922049cb04bf1494a0c7805a4fd1d881505fad
Author: Slava Imameev <slava.imameev@crowdstrike.com>
libbpf: BPF programs manual loading and attaching
This commit adds support for BPF_PROG_LOAD_STRATEGY_MANUAL, allowing BPF
programs to be skipped during the initial bpf_object load and loaded or
unloaded on demand via bpf_program__load() and bpf_program__unload().
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index e2cea1bca1d2d..1a5b582d63b24 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -1227,6 +1228,14 @@ static int bpf_object_adjust_struct_ops_autoload(struct bpf_object *obj)
> if (prog->type != BPF_PROG_TYPE_STRUCT_OPS)
> continue;
>
> + /*
> + * If the user set the load strategy to MANUAL, don't guess it
> + * from map autocreate: a MANUAL program is loaded explicitly
> + * with bpf_program__load(), which rejects any other strategy.
> + */
> + if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
> + continue;
[Severity: Medium]
Can bpf_map__init_kern_struct_ops() overwrite a struct_ops program's
BPF_PROG_LOAD_STRATEGY_MANUAL setting before this check in
bpf_object_adjust_struct_ops_autoload() is reached?
During bpf_object_prepare(), bpf_object__init_kern_struct_ops_maps() runs
immediately before bpf_object_adjust_struct_ops_autoload():
tools/lib/bpf/libbpf.c:bpf_object_prepare() {
...
err = err ? : bpf_object__init_kern_struct_ops_maps(obj);
err = err ? : bpf_object_adjust_struct_ops_autoload(obj);
...
}
Inside bpf_object__init_kern_struct_ops_maps() ->
bpf_map__init_kern_struct_ops(), if a callback member is absent in the
running kernel BTF (with a zeroed shadow field) or if its slot in a
struct_ops map's shadow struct was cleared or replaced, load_strategy is
unconditionally reset to BPF_PROG_LOAD_STRATEGY_DISABLED:
tools/lib/bpf/libbpf.c:bpf_map__init_kern_struct_ops() {
...
if (!kern_member) {
...
if (st_ops->progs[i]) {
st_ops->progs[i]->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;
st_ops->progs[i] = NULL;
}
...
}
...
if (btf_is_ptr(mtype)) {
prog = *(void **)mdata;
if (st_ops->progs[i] && st_ops->progs[i] != prog)
st_ops->progs[i]->load_strategy = BPF_PROG_LOAD_STRATEGY_DISABLED;
...
}
When a user sets BPF_PROG_LOAD_STRATEGY_MANUAL via
bpf_program__set_load_strategy(), those assignments clobber load_strategy to
BPF_PROG_LOAD_STRATEGY_DISABLED.
Then bpf_object_adjust_struct_ops_autoload() does not see
BPF_PROG_LOAD_STRATEGY_MANUAL, so it either leaves the program as
BPF_PROG_LOAD_STRATEGY_DISABLED (causing a subsequent bpf_program__load() to
fail with -EINVAL) or sets it to BPF_PROG_LOAD_STRATEGY_AUTO if another
autocreated struct_ops map still references it.
Should bpf_map__init_kern_struct_ops() preserve load_strategy when it is set
to BPF_PROG_LOAD_STRATEGY_MANUAL?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261029223348.3730738-1-andrey.grodzovsky@crowdstrike.com?part=4
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-10-29 22:54 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-29 22:33 [PATCH bpf-next v9 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 2/7] libbpf: Free func_info, line_info and subprogs in bpf_program__exit() Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 3/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 4/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
2026-10-29 22:54 ` sashiko-bot
2026-10-29 22:33 ` [PATCH bpf-next v9 5/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 6/7] selftests/bpf: Cover BPF program load strategies and manual loading Andrey Grodzovsky
2026-10-29 22:33 ` [PATCH bpf-next v9 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL Andrey Grodzovsky
2026-10-29 22:45 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.