* [PATCH bpf-next v8 0/7] libbpf: BPF program manual loading
@ 2026-10-24 1:45 Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
` (6 more replies)
0 siblings, 7 replies; 13+ messages in thread
From: Andrey Grodzovsky @ 2026-10-24 1:45 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, 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 v8 fixes a patch that was accidentally corrupted during previous send. No
other changes
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, a module kfunc deferred load, and
gen_loader rejection
7/7: convert veristat to use manual programs instead of cloning
Changes since v7 [2]:
- Fix 3/7: its libbpf.h hunk was accidentally corrupted when v7 was
sent, so the series did not apply cleanly and failed CI
[1] https://lore.kernel.org/bpf/20250122215206.59859-1-slava.imameev@crowdstrike.com/
[2] https://lore.kernel.org/bpf/20261022223212.1217849-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 | 265 ++++++++++--
tools/lib/bpf/libbpf.h | 89 ++++
tools/lib/bpf/libbpf.map | 4 +
.../selftests/bpf/prog_tests/load_type.c | 154 +++++++
.../selftests/bpf/prog_tests/manual_load.c | 393 ++++++++++++++++++
.../selftests/bpf/prog_tests/signed_loader.c | 32 ++
.../bpf/prog_tests/test_struct_ops_arena.c | 48 ++-
.../selftests/bpf/progs/test_load_type.c | 31 ++
.../selftests/bpf/progs/test_manual_load.c | 70 ++++
tools/testing/selftests/bpf/veristat.c | 20 +-
10 files changed, 1051 insertions(+), 55 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: 4169c9c90532df9585c0a2ae513337a58f5e59ab
--
2.34.1
^ permalink raw reply [flat|nested] 13+ messages in thread
* [PATCH bpf-next v8 1/7] libbpf: Reject gen_loader set after PREPARE step.
2026-10-24 1:45 [PATCH bpf-next v8 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
@ 2026-10-24 1:45 ` Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 2/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
` (5 subsequent siblings)
6 siblings, 0 replies; 13+ messages in thread
From: Andrey Grodzovsky @ 2026-10-24 1:45 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, 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] 13+ messages in thread
* [PATCH bpf-next v8 2/7] libbpf: BPF program load strategy enum
2026-10-24 1:45 [PATCH bpf-next v8 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
@ 2026-10-24 1:45 ` Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 3/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
` (4 subsequent siblings)
6 siblings, 0 replies; 13+ messages in thread
From: Andrey Grodzovsky @ 2026-10-24 1:45 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, 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 6bb5887090cd..e4cdbc2f467d 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 */
@@ -957,11 +957,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;
@@ -1248,7 +1248,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;
@@ -1339,7 +1340,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;
}
@@ -1377,7 +1378,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;
@@ -3812,7 +3813,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;
@@ -6501,7 +6502,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
@@ -8172,7 +8173,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);
@@ -8208,7 +8209,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 */
@@ -9300,8 +9301,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;
@@ -10093,7 +10094,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,
@@ -10777,16 +10779,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)
@@ -16175,7 +16174,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 */
@@ -16285,3 +16284,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] 13+ messages in thread
* [PATCH bpf-next v8 3/7] libbpf: BPF programs manual loading and attaching
2026-10-24 1:45 [PATCH bpf-next v8 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 2/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
@ 2026-10-24 1:45 ` Andrey Grodzovsky
2026-10-24 2:03 ` sashiko-bot
2026-10-24 3:43 ` Alexei Starovoitov
2026-10-24 1:45 ` [PATCH bpf-next v8 4/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
` (3 subsequent siblings)
6 siblings, 2 replies; 13+ messages in thread
From: Andrey Grodzovsky @ 2026-10-24 1:45 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, 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.
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 | 205 ++++++++++++++++++++++++++++++++++-----
tools/lib/bpf/libbpf.h | 56 +++++++++++
tools/lib/bpf/libbpf.map | 1 +
3 files changed, 240 insertions(+), 22 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index e4cdbc2f467d..1d5d59b95c4e 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;
@@ -870,7 +871,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;
@@ -882,12 +883,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);
@@ -1228,6 +1247,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;
@@ -5874,6 +5901,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);
@@ -9330,6 +9371,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;
@@ -9356,6 +9399,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
*/
@@ -9556,7 +9611,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;
}
@@ -9947,7 +10002,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;
@@ -9963,16 +10018,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];
@@ -9983,7 +10054,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;
@@ -9999,10 +10072,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);
@@ -10019,15 +10101,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)
@@ -10039,7 +10148,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);
@@ -10122,7 +10232,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) {
@@ -10590,7 +10700,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;
@@ -10599,7 +10709,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++)
@@ -10925,7 +11034,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;
@@ -10944,7 +11059,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;
@@ -13730,7 +13846,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;
}
@@ -16287,10 +16403,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);
- prog->load_strategy = strategy;
+ 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);
+ }
+
return 0;
}
@@ -16298,3 +16430,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..dd5380bc355e 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;
@@ -1055,8 +1066,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 +2164,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 +2196,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] 13+ messages in thread
* [PATCH bpf-next v8 4/7] libbpf: Support declarative manual load via SEC("!...") prefix
2026-10-24 1:45 [PATCH bpf-next v8 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (2 preceding siblings ...)
2026-10-24 1:45 ` [PATCH bpf-next v8 3/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
@ 2026-10-24 1:45 ` Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 5/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
` (2 subsequent siblings)
6 siblings, 0 replies; 13+ messages in thread
From: Andrey Grodzovsky @ 2026-10-24 1:45 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, 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 1d5d59b95c4e..ad260cc13203 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -974,11 +974,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] 13+ messages in thread
* [PATCH bpf-next v8 5/7] selftests/bpf: Cover BPF program load strategy transitions
2026-10-24 1:45 [PATCH bpf-next v8 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (3 preceding siblings ...)
2026-10-24 1:45 ` [PATCH bpf-next v8 4/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
@ 2026-10-24 1:45 ` Andrey Grodzovsky
2026-10-24 1:54 ` sashiko-bot
2026-10-24 2:23 ` bot+bpf-ci
2026-10-24 1:45 ` [PATCH bpf-next v8 6/7] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL Andrey Grodzovsky
6 siblings, 2 replies; 13+ messages in thread
From: Andrey Grodzovsky @ 2026-10-24 1:45 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, 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] 13+ messages in thread
* [PATCH bpf-next v8 6/7] selftests/bpf: Cover BPF program manual loading
2026-10-24 1:45 [PATCH bpf-next v8 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (4 preceding siblings ...)
2026-10-24 1:45 ` [PATCH bpf-next v8 5/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
@ 2026-10-24 1:45 ` Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL Andrey Grodzovsky
6 siblings, 0 replies; 13+ messages in thread
From: Andrey Grodzovsky @ 2026-10-24 1:45 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, 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, a deferred load of a module BTF attach target and test of a
program calling a module kfunc.
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 | 393 ++++++++++++++++++
.../selftests/bpf/prog_tests/signed_loader.c | 32 ++
.../selftests/bpf/progs/test_manual_load.c | 70 ++++
3 files changed, 495 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..78a7b2904a76
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/manual_load.c
@@ -0,0 +1,393 @@
+// 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
+
+/*
+ * 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() 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);
+}
+
+/*
+ * 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("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] 13+ messages in thread
* [PATCH bpf-next v8 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL
2026-10-24 1:45 [PATCH bpf-next v8 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
` (5 preceding siblings ...)
2026-10-24 1:45 ` [PATCH bpf-next v8 6/7] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
@ 2026-10-24 1:45 ` Andrey Grodzovsky
6 siblings, 0 replies; 13+ messages in thread
From: Andrey Grodzovsky @ 2026-10-24 1:45 UTC (permalink / raw)
To: bpf, andrii; +Cc: ast, 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.
Assisted-by: Claude:claude-sonnet-5
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] 13+ messages in thread
* Re: [PATCH bpf-next v8 5/7] selftests/bpf: Cover BPF program load strategy transitions
2026-10-24 1:45 ` [PATCH bpf-next v8 5/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
@ 2026-10-24 1:54 ` sashiko-bot
2026-10-24 2:23 ` bot+bpf-ci
1 sibling, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-10-24 1:54 UTC (permalink / raw)
To: Andrey Grodzovsky, bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] selftests/bpf: redundant standalone load_type test duplicates autoload test
--
commit a706cefdc370f05d909294879af1c74efbca1c5c
Author: Slava Imameev <slava.imameev@crowdstrike.com>
selftests/bpf: Cover BPF program load strategy transitions
This commit adds a standalone load_type selftest covering BPF program load
strategy transitions between disabled, auto, and manual modes,
bpf_program__set_autoload() compatibility, and the manual load and attach
cycle after the object is loaded.
> diff --git a/tools/testing/selftests/bpf/prog_tests/load_type.c b/tools/testing/selftests/bpf/prog_tests/load_type.c
> new file mode 100644
> index 0000000000000..7e344c0b04450
> --- /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;
[ ... ]
> 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 0000000000000..3d9b81691d7ad
> --- /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;
> +}
[Severity: Low]
tools/testing/selftests/bpf/prog_tests/autoload.c and
tools/testing/selftests/bpf/progs/test_autoload.c already exist in the same
directories to test bpf_program__set_autoload() and selective program
loading using prog1, prog2, prog3 and prog1_called, prog2_called,
prog3_called:
tools/testing/selftests/bpf/prog_tests/autoload.c:test_autoload() {
int duration = 0, err;
struct test_autoload* skel;
...
/* don't load prog3 */
bpf_program__set_autoload(skel->progs.prog3, false);
}
Since bpf_program__set_autoload() is a thin wrapper around
bpf_program__set_load_strategy(), and commit ("selftests/bpf: Cover BPF
program manual loading") in the same series also adds
tools/testing/selftests/bpf/prog_tests/manual_load.c and
tools/testing/selftests/bpf/progs/test_manual_load.c with the same
prog1, prog2, prog3 raw_tp/sys_enter programs and post-load manual
load/attach sequence in manual_load_verify_main_cycle(), could these
bpf_program__set_load_strategy() transition checks extend test_autoload()
in prog_tests/autoload.c or be consolidated as a subtest in
prog_tests/manual_load.c instead of adding standalone prog_tests/load_type.c
and progs/test_load_type.c files?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261024014514.2959514-1-andrey.grodzovsky@crowdstrike.com?part=5
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH bpf-next v8 3/7] libbpf: BPF programs manual loading and attaching
2026-10-24 1:45 ` [PATCH bpf-next v8 3/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
@ 2026-10-24 2:03 ` sashiko-bot
2026-10-24 3:43 ` Alexei Starovoitov
1 sibling, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-10-24 2:03 UTC (permalink / raw)
To: Andrey Grodzovsky, bpf
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] libbpf: bpf_object__free_relocs() frees reloc_desc for manual progs
- [Medium] libbpf: bpf_program__load() reload fails for jump and func_ptr maps
- [Medium] libbpf: bpf_program__load() allows loading after failed prepare/load
- [Medium] libbpf: bpf_map__init_kern_struct_ops() clobbers manual load strategy
--
commit 2dd11a6f941c4fbe6a6982f6a14ae6a049d19390
Author: Slava Imameev <slava.imameev@crowdstrike.com>
libbpf: BPF programs manual loading and attaching
This commit adds BPF_PROG_LOAD_STRATEGY_MANUAL and bpf_program__load() so
that designated BPF programs can be skipped during bpf_object__load() and
loaded, unloaded, and reloaded on demand afterwards.
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index e4cdbc2f467db..1d5d59b95c4e8 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -882,12 +883,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;
> + }
[Severity: Medium]
What happens when a BPF_PROG_LOAD_STRATEGY_MANUAL program that uses jump
tables or indirect function call tables in read-only data is unloaded here
and then reloaded via bpf_program__load()?
During bpf_object_prepare() -> bpf_object__relocate() ->
bpf_object__relocate_data(), libbpf creates per-program frozen internal
maps once via create_jt_map() (for RELO_INSN_ARRAY) and
create_func_ptr_map() (when map->autocreate && map->func_ptr_cnt &&
prog->has_callx), patching their file descriptors directly into
prog->insns:
tools/lib/bpf/libbpf.c:bpf_object__relocate_data() {
...
if (map->autocreate && map->func_ptr_cnt && prog->has_callx) {
int map_fd;
/* the program gets its own map with pointers to its functions */
map_fd = create_func_ptr_map(obj, prog, relo->map_idx);
...
insn[0].imm = map_fd;
}
...
}
On the first bpf_program__load() call, the kernel verifier claims these
maps permanently:
1. For jump table maps, bpf_insn_array_init() sets insn_array->used to 1 via
atomic_xchg(&insn_array->used, 1), which is only released on verifier
failure and never cleared when a loaded program is later freed.
2. For function pointer maps, resolve_func_ptrs() sets BPF_MAP_USER_PATCHED
on map->user and overwrites the subprogram byte offsets in the frozen
map in-place with JITed kernel addresses; __bpf_free_used_maps() does
not clear BPF_MAP_USER_PATCHED or restore the offsets when the program
is closed.
When bpf_program__unload() closes prog->fd and bpf_program__load() is
called again, prog->insns still references those same map file descriptors.
Won't the kernel verifier reject the reload with -EBUSY in
bpf_insn_array_init() or bpf_map_claim()?
[ ... ]
> @@ -1228,6 +1247,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 prog->load_strategy before
bpf_object_adjust_struct_ops_autoload() reaches this check?
In bpf_object_prepare(), bpf_object__init_kern_struct_ops_maps() ->
bpf_map__init_kern_struct_ops() runs immediately before
bpf_object_adjust_struct_ops_autoload(). Inside
bpf_map__init_kern_struct_ops(), if a struct_ops member is absent in the
running kernel's BTF and zeroed in shadow data, or if the slot was cleared
or replaced in shadow data, it unconditionally resets load_strategy 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;
...
}
If the user explicitly set BPF_PROG_LOAD_STRATEGY_MANUAL on that program via
bpf_program__set_load_strategy(), won't bpf_map__init_kern_struct_ops()
clobber it to BPF_PROG_LOAD_STRATEGY_DISABLED (or cause
bpf_object_adjust_struct_ops_autoload() to set it to
BPF_PROG_LOAD_STRATEGY_AUTO if referenced by another autocreated map)?
[ ... ]
> @@ -16298,3 +16430,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);
[Severity: Medium]
Is checking obj->state < OBJ_PREPARED sufficient if bpf_object_prepare() or
bpf_object_load() previously failed?
When bpf_object_prepare() or bpf_object_load() fails, it tears down the
object via bpf_object_unload(obj) and sets obj->state = OBJ_LOADED:
tools/lib/bpf/libbpf.c:bpf_object_prepare() {
...
if (err) {
bpf_object_unpin(obj);
bpf_object_unload(obj);
obj->state = OBJ_LOADED;
return err;
}
}
Because OBJ_LOADED is greater than OBJ_PREPARED, a subsequent call to
bpf_program__load() passes this state check and attempts to load the
program after bpf_object_unload() has already closed all map file
descriptors and called bpf_program_unload_full() (freeing prog->func_info,
prog->line_info, and prog->subprogs to NULL while leaving their counts
non-zero, or before prog->insns was relocated and sanitized).
> +
> + 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);
> + }
[Severity: Medium]
Could CO-RE relocation failure diagnostics in the verifier log be lost here
when bpf_program__load() is called after bpf_object__load()?
During bpf_object_prepare() -> bpf_object__relocate() ->
bpf_object__relocate_core(), CO-RE relocations are recorded in
prog->reloc_desc for all non-disabled programs, and unguarded failing CO-RE
instructions are poisoned with call 195896080.
However, at the end of bpf_object__load_progs(), libbpf calls
bpf_object__free_relocs(obj), which frees prog->reloc_desc and zeroes
prog->nr_reloc for all programs in the object, including
BPF_PROG_LOAD_STRATEGY_MANUAL programs that were skipped during the
autoload loop:
tools/lib/bpf/libbpf.c:bpf_object__free_relocs() {
...
for (i = 0; i < obj->nr_programs; i++) {
prog = &obj->programs[i];
zfree(&prog->reloc_desc);
prog->nr_reloc = 0;
}
}
When bpf_program__load() later calls bpf_object_load_prog() and
verification fails on a poisoned CO-RE relocation, fixup_verifier_log() ->
fixup_log_failed_core_relo() -> find_relo_core() searches prog->reloc_desc
up to prog->nr_reloc, finds nothing, and leaves the raw poisoned call error
in the verifier log instead of reporting the failed CO-RE relocation
details.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261024014514.2959514-1-andrey.grodzovsky@crowdstrike.com?part=3
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH bpf-next v8 5/7] selftests/bpf: Cover BPF program load strategy transitions
2026-10-24 1:45 ` [PATCH bpf-next v8 5/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
2026-10-24 1:54 ` sashiko-bot
@ 2026-10-24 2:23 ` bot+bpf-ci
1 sibling, 0 replies; 13+ messages in thread
From: bot+bpf-ci @ 2026-10-24 2:23 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: ast, martin.kelly, slava.imameev, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1433 bytes --]
> selftests/bpf: Cover BPF program load strategy transitions
TL;DR: Nit only: the new selftest is named load_type, but the API it tests
says "load strategy"; renaming it to load_strategy would make it easier to
find.
> 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)
> +{
This isn't a bug, but the API this test exercises is named load strategy
throughout (enum bpf_prog_load_strategy, bpf_program__set_load_strategy(),
bpf_program__load_strategy()), and the commit subject says "load strategy"
as well. The string load_type doesn't appear anywhere else in tools/lib/bpf
or the selftests.
Would naming this test load_strategy (prog_tests/load_strategy.c,
progs/test_load_strategy.c) make it easier to find with
'./test_progs -t load_strategy' or by grepping for tests of
bpf_program__set_load_strategy()?
---
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/40052381836
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH bpf-next v8 3/7] libbpf: BPF programs manual loading and attaching
2026-10-24 1:45 ` [PATCH bpf-next v8 3/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
2026-10-24 2:03 ` sashiko-bot
@ 2026-10-24 3:43 ` Alexei Starovoitov
2026-10-28 2:06 ` Andrey Grodzovsky
1 sibling, 1 reply; 13+ messages in thread
From: Alexei Starovoitov @ 2026-10-24 3:43 UTC (permalink / raw)
To: Andrey Grodzovsky, bpf, andrii; +Cc: martin.kelly, slava.imameev
On Fri, Oct 23, 2026 at 06:45 PM Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com> wrote:
> -void bpf_program__unload(struct bpf_program *prog)
> +static void bpf_program_unload_full(struct bpf_program *prog)
[...]
> + if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {
> + zclose(prog->fd);
> + return;
> + }
> +
> + bpf_program_unload_full(prog);
No need for two kinds of unload.
func_info, line_info and subprogs are filled by bpf_object__relocate().
Nothing depends on them being freed in bpf_program__unload().
Let it close the fd of any prog and free the rest in
bpf_program__exit(). Then bpf_program_unload_full(), the strategy check
and the second paragraph of the new doc in libbpf.h go away.
Do it as a prep patch.
Something like this:
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 8fcb8cf..dd15842 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);
> - bpf_object_post_load_cleanup(obj);
> + bpf_object_post_load_cleanup(obj, true);
I really dislike bools.
bpf_object__close() is the only caller that passes true.
Clear obj->has_manual_progs there before the call.
> + err = bpf_object_load_prog(obj, prog, prog->insns, prog->insns_cnt,
> + obj->license, obj->kern_version, &prog->fd);
After bpf_object__load() this loses the fixup of the verifier log.
bpf_object__load_progs() ends with bpf_object__free_relocs(), which
frees reloc_desc of every prog, MANUAL ones too.
When a MANUAL prog has a CO-RE relocation that didn't resolve on this
kernel, fixup_verifier_log() -> find_relo_core() finds nothing
and the user sees
invalid func unknown#195896080
instead of
failed to resolve CO-RE relocation <byte_off> [..] struct foo.bar
The same prog loaded between bpf_object__prepare() and
bpf_object__load() gets the proper message.
Progs that depend on what the kernel supports is the use case
from the cover letter.
Keep reloc_desc of MANUAL progs. bpf_program__exit() frees it.
pw-bot: cr
^ permalink raw reply related [flat|nested] 13+ messages in thread
* Re: [PATCH bpf-next v8 3/7] libbpf: BPF programs manual loading and attaching
2026-10-24 3:43 ` Alexei Starovoitov
@ 2026-10-28 2:06 ` Andrey Grodzovsky
0 siblings, 0 replies; 13+ messages in thread
From: Andrey Grodzovsky @ 2026-10-28 2:06 UTC (permalink / raw)
To: Alexei Starovoitov; +Cc: bpf, andrii, martin.kelly, slava.imameev
On Fri, Oct 23, 2026 at 11:45 PM Alexei Starovoitov
<alexei.starovoitov@gmail.com> wrote:
>
> On Fri, Oct 23, 2026 at 06:45 PM Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com> wrote:
> > -void bpf_program__unload(struct bpf_program *prog)
> > +static void bpf_program_unload_full(struct bpf_program *prog)
>
> [...]
>
> > + if (prog->load_strategy == BPF_PROG_LOAD_STRATEGY_MANUAL) {
> > + zclose(prog->fd);
> > + return;
> > + }
> > +
> > + bpf_program_unload_full(prog);
>
> No need for two kinds of unload.
> func_info, line_info and subprogs are filled by bpf_object__relocate().
> Nothing depends on them being freed in bpf_program__unload().
> Let it close the fd of any prog and free the rest in
> bpf_program__exit(). Then bpf_program_unload_full(), the strategy check
> and the second paragraph of the new doc in libbpf.h go away.
> Do it as a prep patch.
> Something like this:
>
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index 8fcb8cf..dd15842 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);
>
>
> > - bpf_object_post_load_cleanup(obj);
> > + bpf_object_post_load_cleanup(obj, true);
>
> I really dislike bools.
> bpf_object__close() is the only caller that passes true.
> Clear obj->has_manual_progs there before the call.
IMO obj->has_manual_progs naming becomes now somewhat misleading as we
still can have manul progs at that point we clear the flag. I am
thinking to rename the flag to skip_post_load_cleanup to reflect more
precisely what the flag's role.
Andrey
>
> > + err = bpf_object_load_prog(obj, prog, prog->insns, prog->insns_cnt,
> > + obj->license, obj->kern_version, &prog->fd);
>
> After bpf_object__load() this loses the fixup of the verifier log.
> bpf_object__load_progs() ends with bpf_object__free_relocs(), which
> frees reloc_desc of every prog, MANUAL ones too.
> When a MANUAL prog has a CO-RE relocation that didn't resolve on this
> kernel, fixup_verifier_log() -> find_relo_core() finds nothing
> and the user sees
> invalid func unknown#195896080
> instead of
> failed to resolve CO-RE relocation <byte_off> [..] struct
> The same prog loaded between bpf_object__prepare() and
> bpf_object__load() gets the proper message.
> Progs that depend on what the kernel supports is the use case
> from the cover letter.
> Keep reloc_desc of MANUAL progs. bpf_program__exit() frees it.
>
> pw-bot: cr
^ permalink raw reply [flat|nested] 13+ messages in thread
end of thread, other threads:[~2026-10-28 2:06 UTC | newest]
Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-24 1:45 [PATCH bpf-next v8 0/7] libbpf: BPF program manual loading Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 1/7] libbpf: Reject gen_loader set after PREPARE step Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 2/7] libbpf: BPF program load strategy enum Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 3/7] libbpf: BPF programs manual loading and attaching Andrey Grodzovsky
2026-10-24 2:03 ` sashiko-bot
2026-10-24 3:43 ` Alexei Starovoitov
2026-10-28 2:06 ` Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 4/7] libbpf: Support declarative manual load via SEC("!...") prefix Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 5/7] selftests/bpf: Cover BPF program load strategy transitions Andrey Grodzovsky
2026-10-24 1:54 ` sashiko-bot
2026-10-24 2:23 ` bot+bpf-ci
2026-10-24 1:45 ` [PATCH bpf-next v8 6/7] selftests/bpf: Cover BPF program manual loading Andrey Grodzovsky
2026-10-24 1:45 ` [PATCH bpf-next v8 7/7] selftests/bpf: Convert veristat to BPF_PROG_LOAD_STRATEGY_MANUAL Andrey Grodzovsky
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox