* [PATCH bpf-next v6 0/7] libbpf: BPF program manual loading
@ 2026-10-10 14:31 Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
` (6 more replies)
0 siblings, 7 replies; 12+ messages in thread
From: Andrey Grodzovsky @ 2026-10-10 14:31 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. This v6 addresses v5's review
feedback.
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.
Patch overview:
1/7: reject bpf_object__gen_loader() once the object is PREPARED, since
the generated loader records its steps during prepare
2/7: replace bpf_program's boolean autoload field with an enum
(bpf_prog_load_strategy: DISABLED/AUTO), so a third state can be
added - preparatory commit with no functional changes
3/7: add BPF_PROG_LOAD_STRATEGY_MANUAL and bpf_program__load()/
bpf_program__unload(), letting a program be loaded and attached
independently of the bulk bpf_object load/attach pass, and
reloaded/reattached multiple times. This also covers the
interactions with gen_loader, prog array slots, struct_ops and the
per-program log setters
4/7: let a program mark itself MANUAL from its own section name via a
SEC("!...") prefix, mirroring the existing SEC("?...") convention
5/7: selftest covering every load-strategy transition
(DISABLED/AUTO/MANUAL) and bpf_program__set_autoload()'s bool
compatibility
6/7: 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
7/7: convert veristat to use manual programs instead of cloning
Changes since v5 [2]:
- Drop the bpf_program__set_autoattach() ABI versioning (Andrii)
- Restore autoattach orthogonality to the load strategy: it is no longer
saved and restored on strategy set (Andrii)
- Reject setting gen_loader once the object is PREPARED, and detect
the MANUAL program vs gen_loader conflict once in bpf_object_prepare()
instead of in the setters (Andrii)
- Simplify bpf_object_cleanup_btf() to a single early return (Andrii)
- Reject a MANUAL program that is a prog array slot target but has not
been loaded yet (sashiko-bot)
- Relax the per-program log level and log buffer setters for MANUAL
programs to work post OBJ_LOADED state so veristat does not lose the
verifier log when bpf_object__prepare() fails (bot+bpf-ci)
- Allow MANUAL mode on struct_ops programs through the imperative API so
veristat can keep verifying them one by one while still rejecting the
declarative SEC("!...") form (bot+bpf-ci)
- Various cosmetic cleanups - comment clean-up and rewording, file
renames to more correctly reflect their content
[1] https://lore.kernel.org/bpf/20250122215206.59859-1-slava.imameev@crowdstrike.com/
[2] https://lore.kernel.org/bpf/20260923232939.1886328-1-andrey.grodzovsky@crowdstrike.com/
Andrey Grodzovsky (4):
libbpf: Reject gen_loader set after PREPARE step.
libbpf: Support declarative manual load via SEC("!...") prefix
selftests/bpf: Cover BPF program manual loading
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 strategy transitions
tools/lib/bpf/libbpf.c | 278 +++++++++++++--
tools/lib/bpf/libbpf.h | 67 +++-
tools/lib/bpf/libbpf.map | 3 +
.../selftests/bpf/prog_tests/load_type.c | 154 ++++++++
.../selftests/bpf/prog_tests/manual_load.c | 336 ++++++++++++++++++
.../selftests/bpf/prog_tests/signed_loader.c | 32 ++
.../selftests/bpf/progs/test_load_type.c | 31 ++
.../selftests/bpf/progs/test_manual_load.c | 54 +++
tools/testing/selftests/bpf/veristat.c | 20 +-
9 files changed, 923 insertions(+), 52 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/load_type.c
create mode 100644 tools/testing/selftests/bpf/prog_tests/manual_load.c
create mode 100644 tools/testing/selftests/bpf/progs/test_load_type.c
create mode 100644 tools/testing/selftests/bpf/progs/test_manual_load.c
base-commit: 15b578b1715d9a318c804350d98af87c203672bc
--
2.34.1
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH bpf-next v6 1/7] libbpf: Reject gen_loader set after PREPARE step.
2026-10-10 14:31 [PATCH bpf-next v6 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
@ 2026-10-10 14:31 ` Andrey Grodzovsky
2026-10-10 15:21 ` bot+bpf-ci
2026-10-10 14:31 ` [PATCH bpf-next v6 2/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
` (5 subsequent siblings)
6 siblings, 1 reply; 12+ messages in thread
From: Andrey Grodzovsky @ 2026-10-10 14:31 UTC (permalink / raw)
To: bpf, andrii, ast; +Cc: martin.kelly, slava.imameev
It's wrong running the loader post prepare
as most of the recording happens there.
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 cb09ded90773..647f205a354d 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -10637,6 +10637,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] 12+ messages in thread
* [PATCH bpf-next v6 2/7] libbpf: BPF program load strategy enum
2026-10-10 14:31 [PATCH bpf-next v6 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
@ 2026-10-10 14:31 ` Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 3/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
` (4 subsequent siblings)
6 siblings, 0 replies; 12+ messages in thread
From: Andrey Grodzovsky @ 2026-10-10 14:31 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 | 2 ++
3 files changed, 68 insertions(+), 20 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 647f205a354d..fb0512e5facf 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 */
@@ -951,11 +951,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;
@@ -1242,7 +1242,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;
@@ -1333,7 +1334,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;
}
@@ -1371,7 +1372,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;
@@ -3806,7 +3807,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;
@@ -6495,7 +6496,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
@@ -8166,7 +8167,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);
@@ -8202,7 +8203,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 */
@@ -9241,8 +9242,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;
@@ -10032,7 +10033,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,
@@ -10716,16 +10718,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)
@@ -16108,7 +16107,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 */
@@ -16218,3 +16217,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 03f794f4adb0..e7255bd4247f 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 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 53b7b591325f..689de28fe436 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -464,6 +464,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__add_loc_param;
btf__add_loc_param_value;
btf__add_loc_proto;
--
2.34.1
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH bpf-next v6 3/7] libbpf: BPF programs manual loading and attaching
2026-10-10 14:31 [PATCH bpf-next v6 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 2/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
@ 2026-10-10 14:31 ` Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 4/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
` (3 subsequent siblings)
6 siblings, 0 replies; 12+ messages in thread
From: Andrey Grodzovsky @ 2026-10-10 14:31 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.
To support veristat conversion from clone to manual loading
a few commits later, several changes to libbpf had to be applied:
2 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.
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.
SEC("!...") style declaration is rejected for them. On the other hand,
bpf_program__set_load_strategy() accepts them because otherwise
veristat is rejecting them.
Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 218 +++++++++++++++++++++++++++++++++++----
tools/lib/bpf/libbpf.h | 38 ++++++-
tools/lib/bpf/libbpf.map | 1 +
3 files changed, 233 insertions(+), 24 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index fb0512e5facf..42fb687a9795 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;
@@ -864,7 +865,7 @@ static Elf_Data *elf_sec_data(const struct bpf_object *obj, Elf_Scn *scn);
static Elf64_Sym *elf_sym_by_idx(const struct bpf_object *obj, size_t idx);
static Elf64_Rel *elf_rel_by_idx(Elf_Data *data, size_t idx);
-void bpf_program__unload(struct bpf_program *prog)
+static void bpf_program_unload_full(struct bpf_program *prog)
{
if (!prog)
return;
@@ -876,12 +877,30 @@ void bpf_program__unload(struct bpf_program *prog)
zfree(&prog->subprogs);
}
+void bpf_program__unload(struct bpf_program *prog)
+{
+ if (!prog)
+ return;
+
+ /*
+ * MANUAL programs retain their data here so bpf_program__load()
+ * can reload them later; object teardown paths call
+ * bpf_program_unload_full() instead to always release it fully.
+ */
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {
+ zclose(prog->fd);
+ return;
+ }
+
+ bpf_program_unload_full(prog);
+}
+
static void bpf_program__exit(struct bpf_program *prog)
{
if (!prog)
return;
- bpf_program__unload(prog);
+ bpf_program_unload_full(prog);
zfree(&prog->name);
zfree(&prog->sec_name);
zfree(&prog->insns);
@@ -1222,6 +1241,15 @@ static int bpf_object_adjust_struct_ops_autoload(struct bpf_object *obj)
if (prog->type != BPF_PROG_TYPE_STRUCT_OPS)
continue;
+ /*
+ * We avoid this for MANUAL programs to allow veristat
+ * conversion from clone() to MANUAL loading. Setting AUTO
+ * for a program would cause it to be rejected during manual
+ * loading.
+ */
+ if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL)
+ continue;
+
for (j = 0; j < obj->nr_maps; ++j) {
const struct btf_type *type;
@@ -5868,6 +5896,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);
@@ -9271,6 +9313,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;
@@ -9297,6 +9341,21 @@ 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.
+ * bpf_program__set_load_strategy() does allow MANUAL for struct_ops
+ * via its imperative API, but only as a narrow accommodation for
+ * veristat, which needs to verify each program in isolation
+ * without ever calling bpf_object__load().
+ */
+ 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
*/
@@ -9497,7 +9556,7 @@ static int bpf_object_unload(struct bpf_object *obj)
}
for (i = 0; i < obj->nr_programs; i++)
- bpf_program__unload(&obj->programs[i]);
+ bpf_program_unload_full(&obj->programs[i]);
return 0;
}
@@ -9888,7 +9947,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;
@@ -9904,16 +9963,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 ast this still will 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];
@@ -9924,7 +9999,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;
@@ -9940,10 +10017,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);
@@ -9960,13 +10046,38 @@ 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 */
- 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)
+ zfree(&obj->fd_array);
/* 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)
@@ -9978,7 +10089,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);
@@ -10061,7 +10173,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) {
@@ -10529,7 +10641,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;
@@ -10538,7 +10650,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++)
@@ -10864,7 +10975,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;
@@ -10883,7 +11000,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;
@@ -13663,7 +13781,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;
}
@@ -16220,10 +16338,41 @@ 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;
+
+ /*
+ * obj->has_manual_prog is set once in bpf_object_prepare_progs()
+ * and never recomputed; once the object is prepared, no transition
+ * into or out of MANUAL may change which programs are MANUAL,
+ * regardless of direction. AUTO<->DISABLED transitions never touch
+ * MANUAL and keep the looser, pre-existing OBJ_LOADED gate.
+ */
+ if (strategy == BPF_PROG_LOAD_STRATEGY_MANUAL ||
+ prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {
+ if (obj->state >= OBJ_PREPARED)
+ return libbpf_err(-EINVAL);
+ } else if (obj->state >= OBJ_LOADED) {
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:
+ 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;
}
@@ -16231,3 +16380,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 e7255bd4247f..8dbed8ec3789 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -473,6 +473,17 @@ 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.
+ *
+ * If the program's load strategy is BPF_PROG_LOAD_STRATEGY_MANUAL, only the
+ * fd is closed and the program's data is retained so it can be reloaded
+ * later via bpf_program__load(). For any other load strategy, the program's
+ * data is also freed and it cannot be reloaded.
+ *
+ * @param prog BPF program to unload
+ */
LIBBPF_API void bpf_program__unload(struct bpf_program *prog);
struct bpf_link;
@@ -1050,9 +1061,12 @@ LIBBPF_API int bpf_program__add_flags(struct bpf_program *prog, __u32 flags);
*/
LIBBPF_API int bpf_program__clear_flags(struct bpf_program *prog, __u32 flags);
-/* Per-program log level and log buffer getters/setters.
+/*
+ * Per-program log level and log buffer getters/setters.
* See bpf_object_open_opts comments regarding log_level and log_buf
- * interactions.
+ * interactions. The setters return -EBUSY once the object is loaded,
+ * except for BPF_PROG_LOAD_STRATEGY_MANUAL programs, which are loaded
+ * independently by bpf_program__load().
*/
LIBBPF_API __u32 bpf_program__log_level(const struct bpf_program *prog);
LIBBPF_API int bpf_program__set_log_level(struct bpf_program *prog, __u32 log_level);
@@ -2122,21 +2136,27 @@ 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,
};
/**
* @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.
+ * Can only be called before the enclosing bpf_object is loaded for
+ * non MANUAL-> non MANUAL transitions. For manual it only
+ * before 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 loaded
+ * @return 0 on success; negative error code error.
*/
LIBBPF_API int bpf_program__set_load_strategy(struct bpf_program *prog,
enum bpf_prog_load_strategy strategy);
@@ -2150,6 +2170,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 beprepared.
+ *
+ * @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 689de28fe436..9835e660350b 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -464,6 +464,7 @@ LIBBPF_1.8.0 {
bpf_program__attach_tracing_multi;
bpf_program__clear_flags;
bpf_program__clone;
+ bpf_program__load;
bpf_program__load_strategy;
bpf_program__set_load_strategy;
btf__add_loc_param;
--
2.34.1
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH bpf-next v6 4/7] libbpf: Support declarative manual load via SEC("!...") prefix
2026-10-10 14:31 [PATCH bpf-next v6 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (2 preceding siblings ...)
2026-10-10 14:31 ` [PATCH bpf-next v6 3/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
@ 2026-10-10 14:31 ` Andrey Grodzovsky
2026-10-10 15:06 ` sashiko-bot
2026-10-10 14:31 ` [PATCH bpf-next v6 5/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
` (2 subsequent siblings)
6 siblings, 1 reply; 12+ messages in thread
From: Andrey Grodzovsky @ 2026-10-10 14:31 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("?...").
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 | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 42fb687a9795..043f220ebb13 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -968,11 +968,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] 12+ messages in thread
* [PATCH bpf-next v6 5/7] selftests/bpf: Cover BPF program load strategy transitions
2026-10-10 14:31 [PATCH bpf-next v6 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (3 preceding siblings ...)
2026-10-10 14:31 ` [PATCH bpf-next v6 4/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
@ 2026-10-10 14:31 ` Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 6/7] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL Andrey Grodzovsky
6 siblings, 0 replies; 12+ messages in thread
From: Andrey Grodzovsky @ 2026-10-10 14:31 UTC (permalink / raw)
To: bpf, andrii, ast; +Cc: martin.kelly, slava.imameev
From: Slava Imameev <slava.imameev@crowdstrike.com>
Add load_type test covering load strategy transitions
(DISABLED/AUTO/MANUAL), set_autoload() bool/enum compatibility
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 | 154 ++++++++++++++++++
.../selftests/bpf/progs/test_load_type.c | 31 ++++
2 files changed, 185 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..7e344c0b0445
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/load_type.c
@@ -0,0 +1,154 @@
+// 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 = NULL;
+ struct test_load_type *skel;
+ int err;
+
+ skel = test_load_type__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;
+
+ /* Test post loaded behaviors */
+ err = test_load_type__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");
+
+ err = test_load_type__attach(skel);
+ if (!ASSERT_OK(err, "skel_attach"))
+ goto cleanup;
+
+ usleep(1);
+
+ /* Post load only AUTOLOAD program is expected to run */
+ 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(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);
+
+ /* Post explicit load and attach, MANUAL program is also expected to run */
+ if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called_again"))
+ goto cleanup;
+
+ /*
+ * detach prog3 explicitly: its link is kept in a local variable, not in
+ * skel->links, so test_load_type__destroy() has nothing to detach
+ */
+ err = bpf_link__destroy(link);
+ ASSERT_OK(err, "link_destroy");
+ link = NULL;
+
+cleanup:
+ if (link)
+ bpf_link__destroy(link);
+ 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] 12+ messages in thread
* [PATCH bpf-next v6 6/7] selftests/bpf: Cover BPF program manual loading
2026-10-10 14:31 [PATCH bpf-next v6 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (4 preceding siblings ...)
2026-10-10 14:31 ` [PATCH bpf-next v6 5/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
@ 2026-10-10 14:31 ` Andrey Grodzovsky
2026-10-10 15:03 ` sashiko-bot
2026-10-10 14:31 ` [PATCH bpf-next v6 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL Andrey Grodzovsky
6 siblings, 1 reply; 12+ messages in thread
From: Andrey Grodzovsky @ 2026-10-10 14:31 UTC (permalink / raw)
To: bpf, andrii, ast; +Cc: martin.kelly, slava.imameev
Add manual load 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__load() fails
with gen loader object and a MANUAL program.
Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
.../selftests/bpf/prog_tests/manual_load.c | 336 ++++++++++++++++++
.../selftests/bpf/prog_tests/signed_loader.c | 32 ++
.../selftests/bpf/progs/test_manual_load.c | 54 +++
3 files changed, 422 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..e5bdf7e0f04e
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/manual_load.c
@@ -0,0 +1,336 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <test_progs.h>
+#include <time.h>
+#include "test_manual_load.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 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;
+
+ 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;
+
+ 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;
+
+ /*
+ * 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() no longer rejects based on load strategy:
+ * calling it on prog2 (AUTO, currently loaded and attached) performs
+ * a full, irreversible unload instead of returning an error
+ */
+ bpf_program__unload(skel->progs.prog2);
+ ASSERT_LT(bpf_program__fd(skel->progs.prog2), 0, "prog2_fd_closed_after_unload");
+
+ /* 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; MANUAL strategy means its data is retained for reload */
+ bpf_program__unload(skel->progs.prog3);
+
+ /* reload prog3 */
+ err = bpf_program__load(skel->progs.prog3);
+ if (!ASSERT_OK(err, "load_reload"))
+ goto cleanup;
+
+ /* reattach prog3 */
+ link = bpf_program__attach(skel->progs.prog3);
+ if (!ASSERT_OK_PTR(link, "reattach"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called_reattach"))
+ goto cleanup;
+
+ 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);
+}
+
+void test_manual_load(void)
+{
+ 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();
+}
+
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index a0f93756e717..0fbaa2d60aaa 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -2168,6 +2168,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,
@@ -2211,6 +2242,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..6cdbfc21ca37
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_manual_load.c
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+bool prog1_called = false;
+bool prog2_called = false;
+bool prog3_called = false;
+bool prog4_called = false;
+__u32 prog5_sz = 0;
+
+SEC("raw_tp/sys_enter")
+int prog1(const void *ctx)
+{
+ prog1_called = true;
+ return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog2(const void *ctx)
+{
+ prog2_called = true;
+ return 0;
+}
+
+SEC("raw_tp/sys_enter")
+int prog3(const void *ctx)
+{
+ prog3_called = true;
+ return 0;
+}
+
+SEC("!raw_tp/sys_enter")
+int prog4(const void *ctx)
+{
+ prog4_called = true;
+ return 0;
+}
+
+/*
+ * disabled at parse time; its attach target is resolved against module
+ * BTF via bpf_program__set_attach_target() before it is switched to
+ * MANUAL and loaded
+ */
+SEC("?fentry")
+int BPF_PROG(prog5, struct file *file, struct kobject *kobj,
+ const struct bin_attribute *bin_attr, char *buf, loff_t off, size_t len)
+{
+ prog5_sz = len;
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
--
2.34.1
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH bpf-next v6 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL
2026-10-10 14:31 [PATCH bpf-next v6 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (5 preceding siblings ...)
2026-10-10 14:31 ` [PATCH bpf-next v6 6/7] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
@ 2026-10-10 14:31 ` Andrey Grodzovsky
2026-10-10 15:08 ` sashiko-bot
6 siblings, 1 reply; 12+ messages in thread
From: Andrey Grodzovsky @ 2026-10-10 14:31 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.
Assisted-by: Claude:claude-sonnet-5
Suggested-by: Andrii Nakryiko <andrii@kernel.org>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/testing/selftests/bpf/veristat.c | 20 ++++++++------------
1 file changed, 8 insertions(+), 12 deletions(-)
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] 12+ messages in thread
* Re: [PATCH bpf-next v6 6/7] selftests/bpf: Cover BPF program manual loading
2026-10-10 14:31 ` [PATCH bpf-next v6 6/7] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
@ 2026-10-10 15:03 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-10-10 15:03 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: flaky test due to missing PID filtering and RCU detach delay
--
commit 5554ecac607fedce9b924955d0f276b639ab57c5
Author: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
Subject: selftests/bpf: Cover BPF program manual loading
This commit adds a manual load test for BPF programs, covering declarative
and imperative manual load strategies, attach/detach/reload cycles, and a
deferred module BTF attach. It also adds a signed loader test to ensure
that manual programs are rejected when a gen loader object is used.
> 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 0000000000000..e5bdf7e0f04ec
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/manual_load.c
[ ... ]
> + /*
> + * 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");
[Severity: Low]
Since tracepoint unregistration uses RCU, could a concurrent syscall on
another CPU trigger the program during the grace period and set
prog3_called to true?
This might cause the test to sporadically fail when run in parallel.
[ ... ]
> 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 0000000000000..6cdbfc21ca379
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/test_manual_load.c
[ ... ]
> +/*
> + * disabled at parse time; its attach target is resolved against module
> + * BTF via bpf_program__set_attach_target() before it is switched to
> + * MANUAL and loaded
> + */
> +SEC("?fentry")
> +int BPF_PROG(prog5, struct file *file, struct kobject *kobj,
> + const struct bin_attribute *bin_attr, char *buf, loff_t off, size_t len)
> +{
> + prog5_sz = len;
> + return 0;
> +}
[Severity: Low]
Does this tracepoint need PID filtering?
When tests are run in parallel, concurrent tests triggering the testmod read
with different lengths could overwrite prog5_sz, which would fail the length
assertion in the test.
Filtering on the test process's PID (e.g., bpf_get_current_pid_tgid() >> 32
== test_pid) might prevent this.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261010143156.379864-1-andrey.grodzovsky@crowdstrike.com?part=6
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next v6 4/7] libbpf: Support declarative manual load via SEC("!...") prefix
2026-10-10 14:31 ` [PATCH bpf-next v6 4/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
@ 2026-10-10 15:06 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-10-10 15:06 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: multi-line comment style violation in bpf_object__init_prog()
--
commit b80dd28fdb1bbcbf64f3eb8617664b84a1b0b738
Author: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
libbpf: Support declarative manual load via SEC("!...") prefix
This commit adds a SEC("!...") section-name prefix, allowing a program to
declare itself manually loaded. It builds upon the existing SEC("?...")
convention and skips the bulk auto-load pass.
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index 42fb687a97954..043f220ebb136 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
> @@ -968,11 +968,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().
> */
[Severity: Low]
This isn't a bug, but the BPF subsystem multi-line comment style guidelines
require the opening /* to be on its own line.
Since this patch extends the multi-line comment in bpf_object__init_prog()
in tools/lib/bpf/libbpf.c, could the opening of the comment be updated to
match the preferred style?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261010143156.379864-1-andrey.grodzovsky@crowdstrike.com?part=4
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next v6 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL
2026-10-10 14:31 ` [PATCH bpf-next v6 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL Andrey Grodzovsky
@ 2026-10-10 15:08 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-10-10 15:08 UTC (permalink / raw)
To: Andrey Grodzovsky; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] bpf: veristat uninitialized memory read of log buffer on early load failure
- [Low] bpf: veristat skips cleanup and info retrieval when BPF program fd is 0
--
commit 9ac34aec88e7eee9b39c53e8055bc5989345ce5b
Author: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
Subject: selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL
This commit updates the veristat utility to verify programs using
BPF_PROG_LOAD_STRATEGY_MANUAL alongside bpf_program__load() and
bpf_program__unload(). It replaces the previous use of bpf_program__clone(),
ensuring RODATA maps are properly bound during verification and subsequently
dropped upon unload.
> 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
[ ... ]
> @@ -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);
[Severity: Medium]
If bpf_program__load() returns an early error (e.g. from libbpf without
calling the kernel), could the dynamically allocated buf remain
uninitialized?
When env.verbose or env.top_src_lines > 0 is set, buf is allocated
via malloc() but the fallback initialization only null-terminates the
static array:
if (env.verbose || env.top_src_lines > 0) {
buf = malloc(buf_sz);
...
verif_log_buf[0] = '\0';
If bpf_program__load() returns early without modifying the buffer,
parse_verif_log(buf, ...) will read this uninitialized memory. Could
buf[0] = '\0' be set directly on buf before loading?
> + fd = err == 0 ? bpf_program__fd(prog) : -1;
[Severity: Low]
If standard input is closed, can bpf_program__load() assign file descriptor
0 to the loaded program?
If fd 0 is assigned, later checks will explicitly skip handling it:
if (fd > 0 && bpf_prog_get_info_by_fd(fd, &info, &info_len) == 0)
This causes the program's JIT statistics to be skipped. Should this check
test for fd >= 0 instead of fd > 0?
> + 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);
[Severity: Low]
Similar to the issue above, if fd is 0, will this prevent the program from
being immediately unloaded and leave it in the kernel until the entire
BPF object is closed?
Should this check also use if (fd >= 0) to ensure cleanup occurs?
> return 0;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261010143156.379864-1-andrey.grodzovsky@crowdstrike.com?part=7
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next v6 1/7] libbpf: Reject gen_loader set after PREPARE step.
2026-10-10 14:31 ` [PATCH bpf-next v6 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
@ 2026-10-10 15:21 ` bot+bpf-ci
0 siblings, 0 replies; 12+ messages in thread
From: bot+bpf-ci @ 2026-10-10 15:21 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii, ast
Cc: martin.kelly, slava.imameev, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1328 bytes --]
> libbpf: Reject gen_loader set after PREPARE step.
TL;DR: Commit message only, not a bug: the changelog could say what breaks
when gen_loader is enabled after bpf_object__prepare() (earlier steps go
unrecorded), and "running the loader" wording is unclear.
> libbpf: Reject gen_loader set after PREPARE step.
>
> It's wrong running the loader post prepare
> as most of the recording happens there.
>
> Suggested-by: Andrii Nakryiko <andrii@kernel.org>
> Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
This isn't a bug, but could the changelog say what actually breaks when
gen_loader is enabled after bpf_object__prepare()?
For example, bpf_object_load() calls bpf_gen__init() and then only records
the program loads, so the BTF load, map creation, and extern and CO-RE
relocation recording done inside bpf_object_prepare() never make it into the
generated loader.
Also, the wording "running the loader" reads as executing the generated
loader, while the code change is about enabling gen_loader recording. Could
that be tightened to describe the recording?
---
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/38061962076
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-10-10 15:21 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-10 14:31 [PATCH bpf-next v6 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
2026-10-10 15:21 ` bot+bpf-ci
2026-10-10 14:31 ` [PATCH bpf-next v6 2/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 3/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 4/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
2026-10-10 15:06 ` sashiko-bot
2026-10-10 14:31 ` [PATCH bpf-next v6 5/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
2026-10-10 14:31 ` [PATCH bpf-next v6 6/7] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
2026-10-10 15:03 ` sashiko-bot
2026-10-10 14:31 ` [PATCH bpf-next v6 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL Andrey Grodzovsky
2026-10-10 15:08 ` 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.