* [PATCH bpf-next v2 0/5] libbpf: BPF program dynamic loading
@ 2026-08-26 18:18 Andrey Grodzovsky
2026-08-26 18:18 ` [PATCH bpf-next v2 1/5] libbpf: BPF program load type enum Andrey Grodzovsky
` (5 more replies)
0 siblings, 6 replies; 20+ messages in thread
From: Andrey Grodzovsky @ 2026-08-26 18:18 UTC (permalink / raw)
To: bpf, andrii; +Cc: martin.kelly, slava.imameev, linux-open-source
This series was originally posted in January 2025 [1] as
a two-patch RFC introducing a per-program tri-state load type
(disabled/auto/dynamic) to let large BPF applications load a subset of
their programs lazily, after the initial bpf_object load.
We looked at the object-wide opt-in flag Andrii suggested as a
lighter-weight alternative to a per-program enum ("all programs may load
later, opt in via bpf_object_open_opts"). Problem was that it collapses two
states that need to stay distinguishable at the level of a single program:[2]
- a program that is not currently loaded but is *expected* to load
later, gated on runtime configuration; and
- a program that is *permanently* not going to load on this kernel
(e.g. it targets a symbol or struct field that doesn't exist on the
running kernel, or it lost out to an alternative implementation and
will never be used on this object instance).
A per-program enum keeps that distinction where it belongs: disabled
stays fully excluded from relocation/map-creation with isolated failure
handling, auto is unchanged, and the new dynamic state is the only one
that opts a given program into later loading -- without implicitly
promoting every other disabled program in the object along with it.
This v2 keeps the same programs and tests we originally proposed, and
addresses two additional requests from Andrii [3]:
1. bpf_program__set_autoload() is converted from bool to the new
enum bpf_prog_load_type, so it remains source- and ABI-compatible
with the ~200 existing bool callers (false->0, true->1 already match
the enum's DISABLED/AUTO numbering), while becoming a thin forwarder
to bpf_program__set_load_type(). It intentionally continues to reject
BPF_PROG_LOAD_TYPE_DYNAMIC, to preserve its original on/off meaning --
dynamic load is only reachable through set_load_type().
2. A declarative way to mark a program dynamic-load-eligible from its
source, as an alternative to an imperative set_load_type() call, using
a new BTF decl_tag.
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 dynamic loading helps in two ways:
- it shortens the initial load, since only the programs actually
needed for the running configuration are loaded and attached up
front instead of the whole object; and
- it lets a single failing program be unloaded and possibly
reloaded (or a feature toggled at runtime) without tearing down
and reloading the entire bpf_object.
Both reduce the time window during which the tool is partially loaded or
inactive -- for a security tool specifically, that is also the time
window during which the system it protects is unprotected.
We have been running this with our internal fork of libbpf in production
for about a year. Depending on kernel and configuration support, around
250 of our programs are potentially loadable on any given system; with
dynamic 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.
[1] https://lore.kernel.org/bpf/20250122215206.59859-1-slava.imameev@crowdstrike.com/
[2] https://lore.kernel.org/bpf/CAOu3gNjg5vQ=t0C5UmStNpK8zutFC7kcYTrKt9aiR8Gia+rdNw@mail.gmail.com/
[3] https://lore.kernel.org/bpf/CAEf4BzZKy5Wv_TxtEqsbzW+v-tO28kgY=9RhXrnbPhOZcVbOSA@mail.gmail.com/
Andrey Grodzovsky (2):
libbpf: Convert bpf_program__set_autoload() to load-type enum
libbpf: Support declarative dynamic load via BTF decl_tag
Slava Imameev (3):
libbpf: BPF program load type enum
libbpf: BPF programs dynamic loading and attaching
selftests/bpf: Cover BPF program dynamic loading
tools/lib/bpf/bpf_helpers.h | 5 +
tools/lib/bpf/libbpf.c | 273 ++++++++++++++++--
tools/lib/bpf/libbpf.h | 32 +-
tools/lib/bpf/libbpf.map | 4 +
.../selftests/bpf/prog_tests/dynamicload.c | 222 ++++++++++++++
.../selftests/bpf/prog_tests/load_type.c | 167 +++++++++++
.../selftests/bpf/progs/test_dynamicload.c | 40 +++
.../selftests/bpf/progs/test_load_type.c | 31 ++
8 files changed, 741 insertions(+), 33 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/dynamicload.c
create mode 100644 tools/testing/selftests/bpf/prog_tests/load_type.c
create mode 100644 tools/testing/selftests/bpf/progs/test_dynamicload.c
create mode 100644 tools/testing/selftests/bpf/progs/test_load_type.c
--
2.34.1
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH bpf-next v2 1/5] libbpf: BPF program load type enum
2026-08-26 18:18 [PATCH bpf-next v2 0/5] libbpf: BPF program dynamic loading Andrey Grodzovsky
@ 2026-08-26 18:18 ` Andrey Grodzovsky
2026-08-26 19:25 ` bot+bpf-ci
2026-09-11 23:43 ` Andrii Nakryiko
2026-08-26 18:18 ` [PATCH bpf-next v2 2/5] libbpf: BPF programs dynamic loading and attaching Andrey Grodzovsky
` (4 subsequent siblings)
5 siblings, 2 replies; 20+ messages in thread
From: Andrey Grodzovsky @ 2026-08-26 18:18 UTC (permalink / raw)
To: bpf, andrii; +Cc: martin.kelly, slava.imameev, linux-open-source
From: Slava Imameev <slava.imameev@crowdstrike.com>
Replacing the boolean field with an enum simplifies the addition
of new load types. Currently, the bpf_program structure defines
the autoload type using a boolean field. This field is now
replaced with an enum, allowing new BPF program loading types
to be introduced by extending the enum value range.
Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 50 +++++++++++++++++++++++++---------------
tools/lib/bpf/libbpf.h | 15 ++++++++++++
tools/lib/bpf/libbpf.map | 2 ++
3 files changed, 48 insertions(+), 19 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index b749c01742ee..821d854d43a9 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_type load_type;
bool autoattach;
bool sym_global;
bool mark_btf_static;
@@ -872,11 +872,11 @@ bpf_object__init_prog(struct bpf_object *obj, struct bpf_program *prog,
* autoload set to false.
*/
if (sec_name[0] == '?') {
- prog->autoload = false;
+ prog->load_type = BPF_PROG_LOAD_TYPE_DISABLED;
/* from now on forget there was ? in section name */
sec_name++;
} else {
- prog->autoload = true;
+ prog->load_type = BPF_PROG_LOAD_TYPE_AUTO;
}
prog->autoattach = true;
@@ -1163,7 +1163,8 @@ static int bpf_object_adjust_struct_ops_autoload(struct bpf_object *obj)
}
}
if (use_cnt)
- prog->autoload = should_load;
+ prog->load_type = should_load ? BPF_PROG_LOAD_TYPE_AUTO
+ : BPF_PROG_LOAD_TYPE_DISABLED;
}
return 0;
@@ -1248,7 +1249,7 @@ static int bpf_map__init_kern_struct_ops(struct bpf_map *map)
* then bpf_object_adjust_struct_ops_autoload() will update its
* autoload accordingly.
*/
- st_ops->progs[i]->autoload = false;
+ st_ops->progs[i]->load_type = BPF_PROG_LOAD_TYPE_DISABLED;
st_ops->progs[i] = NULL;
}
@@ -1287,7 +1288,7 @@ static int bpf_map__init_kern_struct_ops(struct bpf_map *map)
* if user replaced it with another program or NULL
*/
if (st_ops->progs[i] && st_ops->progs[i] != prog)
- st_ops->progs[i]->autoload = false;
+ st_ops->progs[i]->load_type = BPF_PROG_LOAD_TYPE_DISABLED;
/* Update the value from the shadow type */
st_ops->progs[i] = prog;
@@ -3600,7 +3601,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_type == BPF_PROG_LOAD_TYPE_DISABLED)
continue;
if (prog_needs_vmlinux_btf(prog))
return true;
@@ -6194,7 +6195,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_type == BPF_PROG_LOAD_TYPE_DISABLED)
continue;
/* adjust insn_idx from section frame of reference to the local
@@ -7530,7 +7531,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_type == BPF_PROG_LOAD_TYPE_DISABLED)
continue;
err = bpf_object__relocate_calls(obj, prog);
@@ -7566,7 +7567,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_type == BPF_PROG_LOAD_TYPE_DISABLED)
continue;
/* Process data relos for main programs */
@@ -8398,8 +8399,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_type != BPF_PROG_LOAD_TYPE_AUTO) {
+ pr_debug("prog '%s': skipped auto-loading\n", prog->name);
continue;
}
prog->log_level |= log_level;
@@ -9799,16 +9800,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_type == BPF_PROG_LOAD_TYPE_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_type(prog,
+ autoload ? BPF_PROG_LOAD_TYPE_AUTO : BPF_PROG_LOAD_TYPE_DISABLED);
}
bool bpf_program__autoattach(const struct bpf_program *prog)
@@ -15111,7 +15109,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_type != BPF_PROG_LOAD_TYPE_AUTO || !prog->autoattach)
continue;
/* auto-attaching not supported for this program */
@@ -15221,3 +15219,17 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
free(s->progs);
free(s);
}
+
+int bpf_program__set_load_type(struct bpf_program *prog, enum bpf_prog_load_type type)
+{
+ if (prog->obj->state >= OBJ_LOADED)
+ return libbpf_err(-EINVAL);
+
+ prog->load_type = type;
+ return 0;
+}
+
+enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog)
+{
+ return prog->load_type;
+}
diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
index b965ad571540..700634664e32 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -2078,6 +2078,21 @@ 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 type:
+ *
+ * - BPF_PROG_LOAD_TYPE_DISABLED: the program is not loaded.
+ * - BPF_PROG_LOAD_TYPE_AUTO: the program is autoloaded when the bpf_object is loaded.
+ */
+enum bpf_prog_load_type {
+ BPF_PROG_LOAD_TYPE_DISABLED = 0,
+ BPF_PROG_LOAD_TYPE_AUTO,
+};
+
+LIBBPF_API int bpf_program__set_load_type(struct bpf_program *prog,
+ enum bpf_prog_load_type loadtype);
+LIBBPF_API enum bpf_prog_load_type bpf_program__load_type(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 08ab2ea881fb..4d63dac8c3b7 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -462,4 +462,6 @@ LIBBPF_1.8.0 {
bpf_program__clone;
btf__find_by_name_kind_own;
btf__new_empty_opts;
+ bpf_program__load_type;
+ bpf_program__set_load_type;
} LIBBPF_1.7.0;
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH bpf-next v2 2/5] libbpf: BPF programs dynamic loading and attaching
2026-08-26 18:18 [PATCH bpf-next v2 0/5] libbpf: BPF program dynamic loading Andrey Grodzovsky
2026-08-26 18:18 ` [PATCH bpf-next v2 1/5] libbpf: BPF program load type enum Andrey Grodzovsky
@ 2026-08-26 18:18 ` Andrey Grodzovsky
2026-08-26 18:33 ` sashiko-bot
` (2 more replies)
2026-08-26 18:18 ` [PATCH bpf-next v2 3/5] libbpf: Convert bpf_program__set_autoload() to load-type enum Andrey Grodzovsky
` (3 subsequent siblings)
5 siblings, 3 replies; 20+ messages in thread
From: Andrey Grodzovsky @ 2026-08-26 18:18 UTC (permalink / raw)
To: bpf, andrii; +Cc: martin.kelly, slava.imameev, linux-open-source
From: Slava Imameev <slava.imameev@crowdstrike.com>
BPF programs designated as dynamically 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.
Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
---
tools/lib/bpf/libbpf.c | 144 ++++++++++++++++++++++++++++++++++++---
tools/lib/bpf/libbpf.h | 4 ++
tools/lib/bpf/libbpf.map | 2 +
3 files changed, 141 insertions(+), 9 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 821d854d43a9..c3fe1de43210 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -725,6 +725,7 @@ struct bpf_object {
bool has_subcalls;
bool has_rodata;
+ bool has_dynload_progs;
struct bpf_gen *gen_loader;
@@ -8056,7 +8057,7 @@ static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog
log_buf = prog->log_buf;
log_buf_size = prog->log_size;
own_log_buf = false;
- } else if (obj->log_buf) {
+ } else if (obj->log_buf && prog->load_type != BPF_PROG_LOAD_TYPE_DYNAMIC) {
log_buf = obj->log_buf;
log_buf_size = obj->log_size;
own_log_buf = false;
@@ -8403,6 +8404,7 @@ bpf_object__load_progs(struct bpf_object *obj, int log_level)
pr_debug("prog '%s': skipped auto-loading\n", prog->name);
continue;
}
+
prog->log_level |= log_level;
if (obj->gen_loader)
@@ -9076,15 +9078,22 @@ static void bpf_object_cleanup_btf(struct bpf_object *obj)
obj->btf_modules_loaded = false;
zfree(&obj->btf_modules);
- /* clean up vmlinux BTF */
- btf__free(obj->btf_vmlinux);
- obj->btf_vmlinux = NULL;
+ /* The btf_vmlinux data is needed for dynamically loaded programs,
+ * so defer freeing it in that case to the end of the object lifetime.
+ */
+ if (!obj->has_dynload_progs) {
+ btf__free(obj->btf_vmlinux);
+ obj->btf_vmlinux = NULL;
+ }
}
static void bpf_object_post_load_cleanup(struct bpf_object *obj)
{
- /* clean up fd_array */
- zfree(&obj->fd_array);
+ /* The fd array is needed for dynamically loaded programs,
+ * so defer freeing it in that case to the end of the object lifetime.
+ */
+ if (!obj->has_dynload_progs || !obj->fd_array_cnt)
+ zfree(&obj->fd_array);
/* clean up BTF */
bpf_object_cleanup_btf(obj);
@@ -9678,6 +9687,8 @@ void bpf_object__close(struct bpf_object *obj)
close(obj->jumptable_maps[i].fd);
zfree(&obj->jumptable_maps);
+ zfree(&obj->fd_array);
+
free(obj);
}
@@ -9805,8 +9816,16 @@ bool bpf_program__autoload(const struct bpf_program *prog)
int bpf_program__set_autoload(struct bpf_program *prog, bool autoload)
{
- return bpf_program__set_load_type(prog,
- autoload ? BPF_PROG_LOAD_TYPE_AUTO : BPF_PROG_LOAD_TYPE_DISABLED);
+ enum bpf_prog_load_type type = prog->load_type;
+
+ if (autoload)
+ type = BPF_PROG_LOAD_TYPE_AUTO;
+ else if (prog->load_type == BPF_PROG_LOAD_TYPE_AUTO)
+ type = BPF_PROG_LOAD_TYPE_DISABLED;
+ else
+ return 0; /* Otherwise, keep the current load type. */
+
+ return bpf_program__set_load_type(prog, type);
}
bool bpf_program__autoattach(const struct bpf_program *prog)
@@ -15220,12 +15239,73 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
free(s);
}
+static int bpf_program__set_dynamicload(struct bpf_program *prog)
+{
+ struct bpf_object *obj;
+ const char *attach_name;
+
+ obj = prog->obj;
+ if (!obj)
+ return libbpf_err(-EINVAL);
+
+ /* Dynamically-loaded programs are not supported for gen_loader.
+ * This is because bpf_object_load_prog is not called for
+ * dynamicload programs, so dynamicload programs are not visible
+ * to gen_loader. For this reason, prevent calling
+ * bpf_program__set_dynamicload when gen_loader was used to
+ * generate a BPF object loader.
+ * A gen_loader implementation is being called for autoloaded
+ * programs and defines its own model for loading BPF programs.
+ * To pass a BPF program to gen_loader, set the program's load type
+ * to LD_AUTOLOAD.
+ */
+ if (obj->gen_loader)
+ return libbpf_err(-ENOTSUP);
+
+ if (prog_is_subprog(obj, prog))
+ return libbpf_err(-EINVAL);
+
+ attach_name = strchr(prog->sec_name, '/');
+ if (!attach_name || strchr(attach_name, ':')) {
+ /* Only reject programs that require BTF-based attach target
+ * resolution (indicated by the SEC_ATTACH_BTF flag). Such
+ * programs need the section name parsed for the attach target
+ * function name (after '/') and optionally the module name
+ * (before ':') for libbpf_find_attach_btf_id.
+ *
+ * Programs like SEC("classifier"), SEC("socket"), etc. do
+ * not require BTF attach resolution and can safely use
+ * dynamic loading despite having no '/' in their section
+ * name. The BTF guard in libbpf_prepare_prog_load (checking
+ * SEC_ATTACH_BTF) is the authoritative check; this is an
+ * early-reject for programs that would fail there.
+ */
+ long flags = prog->sec_def ?
+ (long)prog->sec_def->cookie : SEC_ATTACH_BTF;
+ if ((flags & SEC_ATTACH_BTF) && !prog->attach_btf_id)
+ return libbpf_err(-EINVAL);
+ }
+
+ obj->has_dynload_progs = true;
+ prog->load_type = BPF_PROG_LOAD_TYPE_DYNAMIC;
+ prog->autoattach = false;
+
+ return 0;
+}
+
int bpf_program__set_load_type(struct bpf_program *prog, enum bpf_prog_load_type type)
{
if (prog->obj->state >= OBJ_LOADED)
return libbpf_err(-EINVAL);
- prog->load_type = type;
+ switch (type) {
+ case BPF_PROG_LOAD_TYPE_DYNAMIC:
+ return bpf_program__set_dynamicload(prog);
+ default:
+ prog->load_type = type;
+ break;
+ }
+
return 0;
}
@@ -15233,3 +15313,49 @@ enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog)
{
return prog->load_type;
}
+
+/*
+ * This function must be called after bpf_object__load_progs.
+ * Dynamically-loaded program data is initialized on object load.
+ * Post-load initialization is not supported.
+ */
+int
+bpf_program__load_dynamically(struct bpf_program *prog, int extra_log_level)
+{
+ int err;
+ struct bpf_object *obj;
+
+ obj = prog->obj;
+ if (!obj || obj->state < OBJ_LOADED)
+ return libbpf_err(-EINVAL);
+
+ if (prog_is_subprog(obj, prog) || prog->load_type != BPF_PROG_LOAD_TYPE_DYNAMIC)
+ return libbpf_err(-EINVAL);
+
+ prog->log_level |= extra_log_level;
+
+ 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 dynamically load: %d\n", prog->name, err);
+ prog->log_level &= ~extra_log_level;
+ return err;
+ }
+
+ prog->log_level &= ~extra_log_level;
+ return 0;
+}
+
+int bpf_program__unload_dynamically(struct bpf_program *prog)
+{
+ int err;
+
+ if (!prog || prog->load_type != BPF_PROG_LOAD_TYPE_DYNAMIC)
+ return libbpf_err(-EINVAL);
+
+ /* Close the file descriptor but retain the program's data to
+ * support reloading the program if it is required again.
+ */
+ err = zclose(prog->fd);
+ return err ? libbpf_err(-errno) : 0;
+}
diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
index 700634664e32..54d755861289 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -2083,15 +2083,19 @@ LIBBPF_API int bpf_program__clone(struct bpf_program *prog, const struct bpf_pro
*
* - BPF_PROG_LOAD_TYPE_DISABLED: the program is not loaded.
* - BPF_PROG_LOAD_TYPE_AUTO: the program is autoloaded when the bpf_object is loaded.
+ * - BPF_PROG_LOAD_TYPE_DYNAMIC: the program is loaded and attached dynamically.
*/
enum bpf_prog_load_type {
BPF_PROG_LOAD_TYPE_DISABLED = 0,
BPF_PROG_LOAD_TYPE_AUTO,
+ BPF_PROG_LOAD_TYPE_DYNAMIC,
};
LIBBPF_API int bpf_program__set_load_type(struct bpf_program *prog,
enum bpf_prog_load_type loadtype);
LIBBPF_API enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog);
+LIBBPF_API int bpf_program__load_dynamically(struct bpf_program *prog, int extra_log_level);
+LIBBPF_API int bpf_program__unload_dynamically(struct bpf_program *prog);
#ifdef __cplusplus
} /* extern "C" */
diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
index 4d63dac8c3b7..a0389c58efe9 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -464,4 +464,6 @@ LIBBPF_1.8.0 {
btf__new_empty_opts;
bpf_program__load_type;
bpf_program__set_load_type;
+ bpf_program__load_dynamically;
+ bpf_program__unload_dynamically;
} LIBBPF_1.7.0;
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH bpf-next v2 3/5] libbpf: Convert bpf_program__set_autoload() to load-type enum
2026-08-26 18:18 [PATCH bpf-next v2 0/5] libbpf: BPF program dynamic loading Andrey Grodzovsky
2026-08-26 18:18 ` [PATCH bpf-next v2 1/5] libbpf: BPF program load type enum Andrey Grodzovsky
2026-08-26 18:18 ` [PATCH bpf-next v2 2/5] libbpf: BPF programs dynamic loading and attaching Andrey Grodzovsky
@ 2026-08-26 18:18 ` Andrey Grodzovsky
2026-08-26 18:34 ` sashiko-bot
` (2 more replies)
2026-08-26 18:18 ` [PATCH bpf-next v2 4/5] libbpf: Support declarative dynamic load via BTF decl_tag Andrey Grodzovsky
` (2 subsequent siblings)
5 siblings, 3 replies; 20+ messages in thread
From: Andrey Grodzovsky @ 2026-08-26 18:18 UTC (permalink / raw)
To: bpf, andrii; +Cc: martin.kelly, slava.imameev, linux-open-source
Convert bpf_program__set_autoload() from bool to enum bpf_prog_load_type,
to unify the API with the new tri-state load type. Reject
BPF_PROG_LOAD_TYPE_DYNAMIC to preserve the original autoload on/off
meaning; only BPF_PROG_LOAD_TYPE_DYNAMIC is dynamic-load-only. Also
restore autoattach when leaving BPF_PROG_LOAD_TYPE_DYNAMIC via
set_load_type(), fixing a bug where a program left dynamic load and
became autoload-eligible but never re-attached.
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 | 29 ++++++++++++++++++-----------
tools/lib/bpf/libbpf.h | 39 +++++++++++++++++++++++++--------------
tools/lib/bpf/libbpf.map | 6 +++---
3 files changed, 46 insertions(+), 28 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index c3fe1de43210..cd1d00c47841 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -9814,18 +9814,12 @@ bool bpf_program__autoload(const struct bpf_program *prog)
return prog->load_type == BPF_PROG_LOAD_TYPE_AUTO;
}
-int bpf_program__set_autoload(struct bpf_program *prog, bool autoload)
+int bpf_program__set_autoload(struct bpf_program *prog, enum bpf_prog_load_type autoload)
{
- enum bpf_prog_load_type type = prog->load_type;
-
- if (autoload)
- type = BPF_PROG_LOAD_TYPE_AUTO;
- else if (prog->load_type == BPF_PROG_LOAD_TYPE_AUTO)
- type = BPF_PROG_LOAD_TYPE_DISABLED;
- else
- return 0; /* Otherwise, keep the current load type. */
+ if (autoload != BPF_PROG_LOAD_TYPE_AUTO && autoload != BPF_PROG_LOAD_TYPE_DISABLED)
+ return libbpf_err(-EINVAL);
- return bpf_program__set_load_type(prog, type);
+ return bpf_program__set_load_type(prog, autoload);
}
bool bpf_program__autoattach(const struct bpf_program *prog)
@@ -15295,15 +15289,28 @@ static int bpf_program__set_dynamicload(struct bpf_program *prog)
int bpf_program__set_load_type(struct bpf_program *prog, enum bpf_prog_load_type type)
{
+ if (!prog)
+ return libbpf_err(-EINVAL);
+
if (prog->obj->state >= OBJ_LOADED)
return libbpf_err(-EINVAL);
switch (type) {
case BPF_PROG_LOAD_TYPE_DYNAMIC:
return bpf_program__set_dynamicload(prog);
- default:
+ case BPF_PROG_LOAD_TYPE_AUTO:
+ case BPF_PROG_LOAD_TYPE_DISABLED:
+ /*
+ * Leaving the dynamic state: set_dynamicload() cleared
+ * autoattach because dynamically loaded programs are attached
+ * explicitly. Restore the default for the target load type.
+ */
+ if (prog->load_type == BPF_PROG_LOAD_TYPE_DYNAMIC)
+ prog->autoattach = true;
prog->load_type = type;
break;
+ default:
+ return libbpf_err(-EINVAL);
}
return 0;
diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
index 54d755861289..a56068f0f112 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -356,6 +356,19 @@ LIBBPF_API int libbpf_attach_type_by_name(const char *name,
LIBBPF_API int libbpf_find_vmlinux_btf_id(const char *name,
enum bpf_attach_type attach_type);
+/*
+ * The program load type:
+ *
+ * - BPF_PROG_LOAD_TYPE_DISABLED: the program is not loaded.
+ * - BPF_PROG_LOAD_TYPE_AUTO: the program is autoloaded when the bpf_object is loaded.
+ * - BPF_PROG_LOAD_TYPE_DYNAMIC: the program is loaded and attached dynamically.
+ */
+enum bpf_prog_load_type {
+ BPF_PROG_LOAD_TYPE_DISABLED = 0,
+ BPF_PROG_LOAD_TYPE_AUTO,
+ BPF_PROG_LOAD_TYPE_DYNAMIC,
+};
+
/* Accessors of bpf_program */
struct bpf_program;
@@ -376,7 +389,18 @@ LIBBPF_API void bpf_program__set_ifindex(struct bpf_program *prog,
LIBBPF_API const char *bpf_program__name(const struct bpf_program *prog);
LIBBPF_API const char *bpf_program__section_name(const struct bpf_program *prog);
LIBBPF_API bool bpf_program__autoload(const struct bpf_program *prog);
-LIBBPF_API int bpf_program__set_autoload(struct bpf_program *prog, bool autoload);
+/**
+ * @brief **bpf_program__set_autoload()** is retained for backwards
+ * compatibility; **bpf_program__set_load_type()** is the preferred API.
+ * The enum's numbering (DISABLED=0, AUTO=1, DYNAMIC=2) matches the previous
+ * bool semantics (false=0, true=1), so existing bool callers keep compiling
+ * and behaving unchanged. Only BPF_PROG_LOAD_TYPE_AUTO and
+ * BPF_PROG_LOAD_TYPE_DISABLED are accepted, preserving the original
+ * autoload on/off meaning; use bpf_program__set_load_type() to set
+ * BPF_PROG_LOAD_TYPE_DYNAMIC.
+ */
+LIBBPF_API int bpf_program__set_autoload(struct bpf_program *prog,
+ enum bpf_prog_load_type autoload);
LIBBPF_API bool bpf_program__autoattach(const struct bpf_program *prog);
LIBBPF_API void bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach);
@@ -2078,19 +2102,6 @@ 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 type:
- *
- * - BPF_PROG_LOAD_TYPE_DISABLED: the program is not loaded.
- * - BPF_PROG_LOAD_TYPE_AUTO: the program is autoloaded when the bpf_object is loaded.
- * - BPF_PROG_LOAD_TYPE_DYNAMIC: the program is loaded and attached dynamically.
- */
-enum bpf_prog_load_type {
- BPF_PROG_LOAD_TYPE_DISABLED = 0,
- BPF_PROG_LOAD_TYPE_AUTO,
- BPF_PROG_LOAD_TYPE_DYNAMIC,
-};
-
LIBBPF_API int bpf_program__set_load_type(struct bpf_program *prog,
enum bpf_prog_load_type loadtype);
LIBBPF_API enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog);
diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
index a0389c58efe9..74312c95d328 100644
--- a/tools/lib/bpf/libbpf.map
+++ b/tools/lib/bpf/libbpf.map
@@ -460,10 +460,10 @@ LIBBPF_1.8.0 {
global:
bpf_program__attach_tracing_multi;
bpf_program__clone;
- btf__find_by_name_kind_own;
- btf__new_empty_opts;
+ bpf_program__load_dynamically;
bpf_program__load_type;
bpf_program__set_load_type;
- bpf_program__load_dynamically;
bpf_program__unload_dynamically;
+ btf__find_by_name_kind_own;
+ btf__new_empty_opts;
} LIBBPF_1.7.0;
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH bpf-next v2 4/5] libbpf: Support declarative dynamic load via BTF decl_tag
2026-08-26 18:18 [PATCH bpf-next v2 0/5] libbpf: BPF program dynamic loading Andrey Grodzovsky
` (2 preceding siblings ...)
2026-08-26 18:18 ` [PATCH bpf-next v2 3/5] libbpf: Convert bpf_program__set_autoload() to load-type enum Andrey Grodzovsky
@ 2026-08-26 18:18 ` Andrey Grodzovsky
2026-08-26 18:33 ` sashiko-bot
2026-08-26 19:25 ` bot+bpf-ci
2026-08-26 18:18 ` [PATCH bpf-next v2 5/5] selftests/bpf: Cover BPF program dynamic loading Andrey Grodzovsky
2026-09-11 23:43 ` [PATCH bpf-next v2 0/5] libbpf: " Andrii Nakryiko
5 siblings, 2 replies; 20+ messages in thread
From: Andrey Grodzovsky @ 2026-08-26 18:18 UTC (permalink / raw)
To: bpf, andrii; +Cc: martin.kelly, slava.imameev, linux-open-source
Add a loadtype:dynamic BTF decl_tag, exposed as __load_dynamic in
bpf_helpers.h, letting a program declare itself dynamically-loaded in
its source instead of requiring an imperative
bpf_program__set_load_type() call. The tag is checked in the existing
per-program loop in bpf_object_init_progs(), right after prog->sec_def
is resolved, and routes through bpf_program__set_load_type() so it
inherits all existing validation. An explicit set_load_type() call
before load still overrides the tag.
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/bpf_helpers.h | 5 +++
tools/lib/bpf/libbpf.c | 90 +++++++++++++++++++++++++++++++------
2 files changed, 82 insertions(+), 13 deletions(-)
diff --git a/tools/lib/bpf/bpf_helpers.h b/tools/lib/bpf/bpf_helpers.h
index 9d160b5b9c0e..52978f0e0453 100644
--- a/tools/lib/bpf/bpf_helpers.h
+++ b/tools/lib/bpf/bpf_helpers.h
@@ -218,6 +218,11 @@ enum libbpf_tristate {
#define __arg_untrusted __attribute((btf_decl_tag("arg:untrusted")))
#define __arg_arena __attribute((btf_decl_tag("arg:arena")))
+/* Mark a BPF program to be loaded dynamically instead of autoloaded,
+ * equivalent to bpf_program__set_load_type(prog, BPF_PROG_LOAD_TYPE_DYNAMIC).
+ */
+#define __load_dynamic __attribute__((btf_decl_tag("loadtype:dynamic")))
+
#ifndef ___bpf_concat
#define ___bpf_concat(a, b) a ## b
#endif
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index cd1d00c47841..5d58dbf25234 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -1126,9 +1126,10 @@ static bool is_valid_st_ops_program(struct bpf_object *obj,
}
/* For each struct_ops program P, referenced from some struct_ops map M,
- * enable P.autoload if there are Ms for which M.autocreate is true,
- * disable P.autoload if for all Ms M.autocreate is false.
- * Don't change P.autoload for programs that are not referenced from any maps.
+ * set P's load type to BPF_PROG_LOAD_TYPE_AUTO if there are Ms for which
+ * M.autocreate is true, or to BPF_PROG_LOAD_TYPE_DISABLED if for all Ms
+ * M.autocreate is false.
+ * Don't change P's load type for programs that are not referenced from any maps.
*/
static int bpf_object_adjust_struct_ops_autoload(struct bpf_object *obj)
{
@@ -1244,11 +1245,12 @@ static int bpf_map__init_kern_struct_ops(struct bpf_map *map)
if (st_ops->progs[i]) {
/* If we had declaratively set struct_ops callback, we need to
- * force its autoload to false, because it doesn't have
- * a chance of succeeding from POV of the current struct_ops map.
- * If this program is still referenced somewhere else, though,
- * then bpf_object_adjust_struct_ops_autoload() will update its
- * autoload accordingly.
+ * force its load type to BPF_PROG_LOAD_TYPE_DISABLED, because
+ * it doesn't have a chance of succeeding from POV of the
+ * current struct_ops map. If this program is still referenced
+ * somewhere else, though, then
+ * bpf_object_adjust_struct_ops_autoload() will update its
+ * load type accordingly.
*/
st_ops->progs[i]->load_type = BPF_PROG_LOAD_TYPE_DISABLED;
st_ops->progs[i] = NULL;
@@ -7156,6 +7158,50 @@ static int bpf_prog_assign_exc_cb(struct bpf_object *obj, struct bpf_program *pr
return 0;
}
+#define DYNLOAD_DECL_TAG "loadtype:dynamic"
+
+/*
+ * Check whether a BPF program is annotated with the DYNLOAD_DECL_TAG BTF
+ * decl tag, requesting that it be loaded dynamically after the object load.
+ *
+ * Returns 1 if the tag is present, 0 if it is not, negative error otherwise.
+ */
+static int prog_has_dynload_tag(struct bpf_object *obj, struct bpf_program *prog)
+{
+ int i, n;
+
+ if (!obj->btf)
+ return 0;
+
+ n = btf__type_cnt(obj->btf);
+ for (i = 1; i < n; i++) {
+ const struct btf_type *tag_t, *fn_t;
+
+ tag_t = btf__type_by_id(obj->btf, i);
+ if (!btf_is_decl_tag(tag_t) || btf_decl_tag(tag_t)->component_idx != -1)
+ continue;
+ if (strcmp(btf__str_by_offset(obj->btf, tag_t->name_off),
+ DYNLOAD_DECL_TAG) != 0)
+ continue;
+
+ /* a decl tag's type field points at the entity it decorates */
+ fn_t = btf__type_by_id(obj->btf, tag_t->type);
+ if (!btf_is_func(fn_t) || btf_func_linkage(fn_t) != BTF_FUNC_GLOBAL) {
+ pr_warn("prog '%s': " DYNLOAD_DECL_TAG
+ " decl tag must be applied to a global function\n",
+ prog->name);
+ return -EINVAL;
+ }
+
+ if (strcmp(btf__str_by_offset(obj->btf, fn_t->name_off), prog->name) != 0)
+ continue; /* tag belongs to a different program */
+
+ return 1;
+ }
+
+ return 0;
+}
+
static struct {
enum bpf_prog_type prog_type;
const char *ctx_name;
@@ -8401,7 +8447,10 @@ bpf_object__load_progs(struct bpf_object *obj, int log_level)
if (prog_is_subprog(obj, prog))
continue;
if (prog->load_type != BPF_PROG_LOAD_TYPE_AUTO) {
- pr_debug("prog '%s': skipped auto-loading\n", prog->name);
+ pr_debug("prog '%s': skipped loading, load type is %s\n",
+ prog->name,
+ prog->load_type == BPF_PROG_LOAD_TYPE_DYNAMIC ?
+ "dynamic" : "disabled");
continue;
}
@@ -8467,6 +8516,22 @@ static int bpf_object_init_progs(struct bpf_object *obj, const struct bpf_object
return err;
}
}
+
+ if (prog_is_subprog(obj, prog))
+ continue;
+
+ err = prog_has_dynload_tag(obj, prog);
+ if (err < 0)
+ return err;
+ if (err > 0) {
+ err = bpf_program__set_load_type(prog, BPF_PROG_LOAD_TYPE_DYNAMIC);
+ if (err) {
+ pr_warn("prog '%s': failed to apply " DYNLOAD_DECL_TAG
+ " decl tag: %s\n",
+ prog->name, errstr(err));
+ return err;
+ }
+ }
}
return 0;
@@ -15248,10 +15313,9 @@ static int bpf_program__set_dynamicload(struct bpf_program *prog)
* to gen_loader. For this reason, prevent calling
* bpf_program__set_dynamicload when gen_loader was used to
* generate a BPF object loader.
- * A gen_loader implementation is being called for autoloaded
- * programs and defines its own model for loading BPF programs.
- * To pass a BPF program to gen_loader, set the program's load type
- * to LD_AUTOLOAD.
+ * A gen_loader implementation is being called for programs with
+ * load type BPF_PROG_LOAD_TYPE_AUTO and defines its own model for
+ * loading BPF programs.
*/
if (obj->gen_loader)
return libbpf_err(-ENOTSUP);
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH bpf-next v2 5/5] selftests/bpf: Cover BPF program dynamic loading
2026-08-26 18:18 [PATCH bpf-next v2 0/5] libbpf: BPF program dynamic loading Andrey Grodzovsky
` (3 preceding siblings ...)
2026-08-26 18:18 ` [PATCH bpf-next v2 4/5] libbpf: Support declarative dynamic load via BTF decl_tag Andrey Grodzovsky
@ 2026-08-26 18:18 ` Andrey Grodzovsky
2026-08-26 18:33 ` sashiko-bot
` (2 more replies)
2026-09-11 23:43 ` [PATCH bpf-next v2 0/5] libbpf: " Andrii Nakryiko
5 siblings, 3 replies; 20+ messages in thread
From: Andrey Grodzovsky @ 2026-08-26 18:18 UTC (permalink / raw)
To: bpf, andrii; +Cc: martin.kelly, slava.imameev, linux-open-source
From: Slava Imameev <slava.imameev@crowdstrike.com>
Add load_type and dynamicload tests: the per-program load type enum
(disabled/auto/dynamic) and its transitions, set_autoload() bool/enum
compatibility and autoattach restore on exit from dynamic load, the
full dynamic load/attach/reattach/unload lifecycle, and the
loadtype:dynamic BTF decl_tag as a declarative alternative to an
imperative set_load_type() call.
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/dynamicload.c | 222 ++++++++++++++++++
.../selftests/bpf/prog_tests/load_type.c | 167 +++++++++++++
.../selftests/bpf/progs/test_dynamicload.c | 40 ++++
.../selftests/bpf/progs/test_load_type.c | 31 +++
4 files changed, 460 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/dynamicload.c
create mode 100644 tools/testing/selftests/bpf/prog_tests/load_type.c
create mode 100644 tools/testing/selftests/bpf/progs/test_dynamicload.c
create mode 100644 tools/testing/selftests/bpf/progs/test_load_type.c
diff --git a/tools/testing/selftests/bpf/prog_tests/dynamicload.c b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
new file mode 100644
index 000000000000..799c43034ff9
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <test_progs.h>
+#include <time.h>
+#include "test_dynamicload.skel.h"
+
+/* prog4 is tagged __load_dynamic in the source instead of being set
+ * imperatively; verify that an explicit bpf_program__set_load_type() call
+ * before load overrides the tag's declarative default.
+ */
+static void dynamicload_verify_override(void)
+{
+ struct test_dynamicload *skel;
+ int err;
+
+ skel = test_dynamicload__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ err = bpf_program__set_load_type(skel->progs.prog4, BPF_PROG_LOAD_TYPE_DISABLED);
+ if (!ASSERT_OK(err, "set_load_type_disabled"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_type(skel->progs.prog4), BPF_PROG_LOAD_TYPE_DISABLED,
+ "prog4_load_type_overridden"))
+ goto cleanup;
+
+ /* keep the other dynamic-load-only programs out of the way of this load */
+ bpf_program__set_load_type(skel->progs.prog1, BPF_PROG_LOAD_TYPE_DISABLED);
+ bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DISABLED);
+
+ err = test_dynamicload__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ /* prog4 was never loaded, so it cannot be loaded dynamically either */
+ err = bpf_program__load_dynamically(skel->progs.prog4, 0);
+ ASSERT_ERR(err, "load_dynamically_after_override");
+
+cleanup:
+ test_dynamicload__destroy(skel);
+}
+
+void test_dynamicload(void)
+{
+ struct bpf_link *link;
+ struct test_dynamicload *skel;
+ int err;
+
+ skel = test_dynamicload__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ /* the __load_dynamic tag alone, with no imperative call, must set
+ * prog4's load type before it is ever touched below
+ */
+ if (!ASSERT_EQ(bpf_program__load_type(skel->progs.prog4), BPF_PROG_LOAD_TYPE_DYNAMIC,
+ "prog4_tag_load_type"))
+ goto cleanup;
+ if (!ASSERT_FALSE(bpf_program__autoattach(skel->progs.prog4), "prog4_autoattach"))
+ goto cleanup;
+
+ /* don't load prog1 */
+ bpf_program__set_load_type(skel->progs.prog1, BPF_PROG_LOAD_TYPE_DISABLED);
+
+ /* prog2 is autoload */
+ bpf_program__set_load_type(skel->progs.prog2, BPF_PROG_LOAD_TYPE_AUTO);
+
+ /* prog3 is dynamically loaded */
+ bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DYNAMIC);
+
+ err = test_dynamicload__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ err = test_dynamicload__attach(skel);
+ if (!ASSERT_OK(err, "skel_attach"))
+ goto cleanup;
+
+ /* trigger the BPF programs */
+ usleep(1);
+
+ ASSERT_FALSE(skel->bss->prog1_called, "prog1_called");
+ ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+ ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+ ASSERT_FALSE(skel->bss->prog4_called, "prog4_called");
+
+ /* prog1 is disabled for load */
+ err = bpf_program__load_dynamically(skel->progs.prog1, 0);
+ if (!ASSERT_ERR(err, "load_dynamically_disabled"))
+ goto cleanup;
+
+ /* prog1 is disabled for load */
+ err = bpf_program__unload_dynamically(skel->progs.prog1);
+ if (!ASSERT_ERR(err, "unload_dynamically_disabled"))
+ goto cleanup;
+
+ /* prog2 is autoload */
+ err = bpf_program__load_dynamically(skel->progs.prog2, 0);
+ if (!ASSERT_ERR(err, "load_dynamically_autoload"))
+ goto cleanup;
+
+ /* prog2 is autoload */
+ err = bpf_program__unload_dynamically(skel->progs.prog2);
+ if (!ASSERT_ERR(err, "unload_dynamically_autoload"))
+ goto cleanup;
+
+ /* reset the call flags */
+ skel->bss->prog2_called = false;
+ skel->bss->prog3_called = false;
+
+ usleep(1);
+
+ ASSERT_FALSE(skel->bss->prog1_called, "prog1_called");
+ ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+ ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+ /* load prog3 */
+ err = bpf_program__load_dynamically(skel->progs.prog3, 0);
+ if (!ASSERT_OK(err, "load_dynamically"))
+ goto cleanup;
+
+ /* attach prog3 */
+ link = bpf_program__attach(skel->progs.prog3);
+ if (!ASSERT_OK_PTR(link, "attach"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called"))
+ goto cleanup;
+
+ /* detach prog3 as test_dynamicload__destroy doesn't detach dynamically loaded programs */
+ err = bpf_link__destroy(link);
+ if (!ASSERT_OK(err, "link_destroy"))
+ goto cleanup;
+
+ /* reset the call flags after detach */
+ skel->bss->prog2_called = false;
+ skel->bss->prog3_called = false;
+
+ usleep(1);
+
+ ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+ ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+ /* unload prog3 */
+ err = bpf_program__unload_dynamically(skel->progs.prog3);
+ if (!ASSERT_OK(err, "unload_dynamically"))
+ goto cleanup;
+
+ /* reload prog3 */
+ err = bpf_program__load_dynamically(skel->progs.prog3, 0);
+ if (!ASSERT_OK(err, "load_dynamically_reload"))
+ goto cleanup;
+
+ /* reattach prog3 */
+ link = bpf_program__attach(skel->progs.prog3);
+ if (!ASSERT_OK_PTR(link, "reattach"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called_reattach"))
+ goto cleanup;
+
+ /* detach prog3 as test_dynamicload__destroy doesn't detach dynamically loaded programs */
+ err = bpf_link__destroy(link);
+ if (!ASSERT_OK(err, "link_destroy_reattach"))
+ goto cleanup;
+
+ /* verify regular unload for dynamically loaded program,
+ * unload prog3 as a regular program
+ */
+ bpf_program__unload(skel->progs.prog3);
+
+ /* reset the call flags after unload */
+ skel->bss->prog2_called = false;
+ skel->bss->prog3_called = false;
+
+ usleep(1);
+
+ ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+ ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+ /* reloading prog3 must fail as it was unloaded as a regular program */
+ err = bpf_program__load_dynamically(skel->progs.prog3, 0);
+ ASSERT_ERR(err, "load_dynamically_after_regular_unload");
+
+ /* run prog4 (declaratively tagged) through the same dynamic
+ * load/attach/trigger/detach/unload cycle as prog3
+ */
+ err = bpf_program__load_dynamically(skel->progs.prog4, 0);
+ if (!ASSERT_OK(err, "prog4_load_dynamically"))
+ goto cleanup;
+
+ link = bpf_program__attach(skel->progs.prog4);
+ if (!ASSERT_OK_PTR(link, "prog4_attach"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog4_called, "prog4_called"))
+ goto cleanup;
+
+ err = bpf_link__destroy(link);
+ if (!ASSERT_OK(err, "prog4_link_destroy"))
+ goto cleanup;
+
+ err = bpf_program__unload_dynamically(skel->progs.prog4);
+ ASSERT_OK(err, "prog4_unload_dynamically");
+
+ test_dynamicload__destroy(skel);
+
+ /* separate scenario: imperative override of the declarative tag */
+ dynamicload_verify_override();
+ return;
+
+cleanup:
+ test_dynamicload__destroy(skel);
+}
+
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..fc91f6666c50
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/load_type.c
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <test_progs.h>
+#include <time.h>
+#include "test_load_type.skel.h"
+
+void test_load_type(void)
+{
+ struct bpf_link *link;
+ struct test_load_type *skel;
+ int err;
+
+ skel = test_load_type__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ /* don't load prog1 */
+ bpf_program__set_load_type(skel->progs.prog1, BPF_PROG_LOAD_TYPE_DISABLED);
+
+ /* load and attach prog2 */
+ bpf_program__set_load_type(skel->progs.prog2, BPF_PROG_LOAD_TYPE_AUTO);
+ if (!ASSERT_TRUE(bpf_program__autoload(skel->progs.prog2), "prog2_autoload"))
+ goto cleanup;
+
+ err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DYNAMIC);
+ if (!ASSERT_OK(err, "set_load_type_dynamic"))
+ goto cleanup;
+ if (!ASSERT_EQ(bpf_program__load_type(skel->progs.prog3), BPF_PROG_LOAD_TYPE_DYNAMIC,
+ "prog3_load_type"))
+ goto cleanup;
+
+ /* bpf_program__set_autoload() is a thin forwarder to set_load_type(),
+ * restricted to AUTO/DISABLED to preserve its original bool on/off
+ * meaning; it does change the load type of a program that isn't
+ * currently BPF_PROG_LOAD_TYPE_AUTO.
+ */
+ err = bpf_program__set_autoload(skel->progs.prog3, false);
+ if (!ASSERT_OK(err, "set_autoload_false"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_type(skel->progs.prog3), BPF_PROG_LOAD_TYPE_DISABLED,
+ "prog3_load_type_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_type(skel->progs.prog3), BPF_PROG_LOAD_TYPE_AUTO,
+ "prog3_load_type_after_true"))
+ goto cleanup;
+
+ /* set_autoload() only accepts AUTO/DISABLED, to preserve its original
+ * on/off meaning; DYNAMIC must go through set_load_type()
+ */
+ err = bpf_program__set_autoload(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DYNAMIC);
+ if (!ASSERT_ERR(err, "set_autoload_dynamic_rejected"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_type(skel->progs.prog3), BPF_PROG_LOAD_TYPE_AUTO,
+ "prog3_load_type_unchanged_after_rejected_autoload"))
+ goto cleanup;
+
+ err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DYNAMIC);
+ if (!ASSERT_OK(err, "set_load_type_dynamic_enum"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_type(skel->progs.prog3), BPF_PROG_LOAD_TYPE_DYNAMIC,
+ "prog3_load_type_after_dynamic_enum"))
+ goto cleanup;
+
+ /* leaving DYNAMIC for AUTO must restore autoattach (regression test for
+ * the autoattach residue bug: set_dynamicload() clears autoattach, and
+ * nothing used to restore it on exit)
+ */
+ err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_AUTO);
+ if (!ASSERT_OK(err, "set_load_type_auto"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_type(skel->progs.prog3), BPF_PROG_LOAD_TYPE_AUTO,
+ "prog3_load_type_auto"))
+ goto cleanup;
+
+ if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog3), "prog3_autoattach_restored"))
+ goto cleanup;
+
+ /* the same residue can relay through DISABLED and resurface on a later
+ * DISABLED -> AUTO transition, so the fix must cover both exit edges
+ */
+ err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DYNAMIC);
+ if (!ASSERT_OK(err, "set_load_type_dynamic_again"))
+ goto cleanup;
+
+ err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DISABLED);
+ if (!ASSERT_OK(err, "set_load_type_disabled"))
+ goto cleanup;
+
+ err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_AUTO);
+ if (!ASSERT_OK(err, "set_load_type_auto_via_disabled"))
+ goto cleanup;
+
+ if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog3),
+ "prog3_autoattach_restored_via_disabled"))
+ goto cleanup;
+
+ /* an out-of-range load type is rejected */
+ err = bpf_program__set_load_type(skel->progs.prog3, (enum bpf_prog_load_type)999);
+ if (!ASSERT_ERR(err, "set_load_type_invalid"))
+ goto cleanup;
+
+ /* change the type back to BPF_PROG_LOAD_TYPE_DYNAMIC for the rest of the test */
+ err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DYNAMIC);
+ if (!ASSERT_OK(err, "set_load_type_dynamic_final"))
+ goto cleanup;
+
+ if (!ASSERT_EQ(bpf_program__load_type(skel->progs.prog3), BPF_PROG_LOAD_TYPE_DYNAMIC,
+ "prog3_load_type_final"))
+ goto cleanup;
+
+ err = test_load_type__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto cleanup;
+
+ if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog2), "prog2_autoattach"))
+ goto cleanup;
+ if (!ASSERT_FALSE(bpf_program__autoattach(skel->progs.prog3), "prog3_autoattach"))
+ goto cleanup;
+
+ /* loaded program type cannot be changed */
+ err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DISABLED);
+ ASSERT_ERR(err, "set_load_type_after_load");
+
+ err = test_load_type__attach(skel);
+ if (!ASSERT_OK(err, "skel_attach"))
+ goto cleanup;
+
+ usleep(1);
+
+ ASSERT_FALSE(skel->bss->prog1_called, "prog1_called");
+ ASSERT_TRUE(skel->bss->prog2_called, "prog2_called");
+ ASSERT_FALSE(skel->bss->prog3_called, "prog3_called");
+
+ err = bpf_program__load_dynamically(skel->progs.prog3, 0);
+ if (!ASSERT_OK(err, "load_dynamically_1"))
+ goto cleanup;
+
+ err = bpf_program__load_dynamically(skel->progs.prog3, 0);
+ if (!ASSERT_OK(err, "load_dynamically_2"))
+ goto cleanup;
+
+ /* attach prog3 */
+ link = bpf_program__attach(skel->progs.prog3);
+ if (!ASSERT_OK_PTR(link, "attach"))
+ goto cleanup;
+
+ usleep(1);
+
+ if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called_again"))
+ goto cleanup;
+
+ /* detach prog3 as test_load_type__destroy doesn't detach dynamically loaded programs */
+ err = bpf_link__destroy(link);
+ ASSERT_OK(err, "link_destroy");
+
+cleanup:
+ test_load_type__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/test_dynamicload.c b/tools/testing/selftests/bpf/progs/test_dynamicload.c
new file mode 100644
index 000000000000..6f5a5cb89803
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_dynamicload.c
@@ -0,0 +1,40 @@
+// 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;
+bool prog4_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;
+}
+
+SEC("raw_tp/sys_enter")
+__load_dynamic
+int prog4(const void *ctx)
+{
+ prog4_called = true;
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
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] 20+ messages in thread
* Re: [PATCH bpf-next v2 2/5] libbpf: BPF programs dynamic loading and attaching
2026-08-26 18:18 ` [PATCH bpf-next v2 2/5] libbpf: BPF programs dynamic loading and attaching Andrey Grodzovsky
@ 2026-08-26 18:33 ` sashiko-bot
2026-08-26 19:38 ` bot+bpf-ci
2026-09-11 23:44 ` Andrii Nakryiko
2 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-08-26 18:33 UTC (permalink / raw)
To: Andrey Grodzovsky; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] `bpf_program__load_dynamically` overwrites `prog->fd` without closing the old one, leading to a file descriptor leak if the program is already loaded.
- [Medium] Public API `bpf_program__load_dynamically` returns a negative error code directly on failure instead of wrapping it in `libbpf_err()`, violating LIBBPF-001.
- [Medium] `bpf_program__load_dynamically` permanently corrupts the user-configured `log_level` state.
- [Medium] The internal static function `bpf_program__set_dynamicload` incorrectly uses the `libbpf_err()` wrapper.
- [Medium] The public API `bpf_program__load_dynamically` dereferences `prog` before verifying it is not NULL.
--
commit d4848d3e91d6da741dd4f244c1469a745b1d0a7e
Author: Slava Imameev <slava.imameev@crowdstrike.com>
libbpf: BPF programs dynamic loading and attaching
BPF programs designated as dynamically loaded can be loaded and
attached independently after the initial bpf_object loading and
attaching. This patch enables these programs to be reloaded and
reattached multiple times, reducing load times for utilities with
many conditionally needed programs.
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index 821d854d43a9f..c3fe1de432108 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -15220,12 +15239,73 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
> free(s);
> }
>
> +static int bpf_program__set_dynamicload(struct bpf_program *prog)
> +{
> + struct bpf_object *obj;
> + const char *attach_name;
> +
> + obj = prog->obj;
> + if (!obj)
> + return libbpf_err(-EINVAL);
[Severity: Medium]
Is this internal static function improperly invoking the public API error
wrapper libbpf_err() instead of returning a negative error code? Because the
internal function wraps the error, errno is set prematurely, and the public
API returns an unwrapped error result.
[ ... ]
> +/*
> + * This function must be called after bpf_object__load_progs.
> + * Dynamically-loaded program data is initialized on object load.
> + * Post-load initialization is not supported.
> + */
> +int
> +bpf_program__load_dynamically(struct bpf_program *prog, int extra_log_level)
> +{
> + int err;
> + struct bpf_object *obj;
> +
> + obj = prog->obj;
[Severity: Medium]
Can this cause a segmentation fault by dereferencing prog before verifying it
is not NULL? Expected behavior for libbpf public APIs is to handle NULL
arguments gracefully (as done in bpf_program__unload_dynamically).
> + if (!obj || obj->state < OBJ_LOADED)
> + return libbpf_err(-EINVAL);
> +
> + if (prog_is_subprog(obj, prog) || prog->load_type != BPF_PROG_LOAD_TYPE_DYNAMIC)
> + return libbpf_err(-EINVAL);
> +
> + prog->log_level |= extra_log_level;
> +
> + err = bpf_object_load_prog(obj, prog, prog->insns, prog->insns_cnt,
> + obj->license, obj->kern_version, &prog->fd);
[Severity: High]
Could this lead to a file descriptor leak if the program is already loaded?
The call appears to unconditionally overwrite the prog->fd memory location
without closing the previous file descriptor.
> + if (err) {
> + pr_warn("prog '%s': failed to dynamically load: %d\n", prog->name, err);
> + prog->log_level &= ~extra_log_level;
> + return err;
[Severity: Medium]
Are we missing a libbpf_err() wrapper here? This return path directly exposes
a raw negative error to the caller, which may leave errno improperly set.
> + }
> +
> + prog->log_level &= ~extra_log_level;
[Severity: Medium]
Will this bitwise operation permanently corrupt the user-configured log
level? If the user previously set bits in prog->log_level that overlap with
extra_log_level, those original bits would be incorrectly cleared here
instead of being restored to their previous state.
> + return 0;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826181825.1826541-1-andrey.grodzovsky@crowdstrike.com?part=2
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 4/5] libbpf: Support declarative dynamic load via BTF decl_tag
2026-08-26 18:18 ` [PATCH bpf-next v2 4/5] libbpf: Support declarative dynamic load via BTF decl_tag Andrey Grodzovsky
@ 2026-08-26 18:33 ` sashiko-bot
2026-08-26 19:25 ` bot+bpf-ci
1 sibling, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-08-26 18:33 UTC (permalink / raw)
To: Andrey Grodzovsky; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] Applying __load_dynamic to programs requiring BTF target resolution causes object opening to unconditionally fail.
- [Medium] prog_has_dynload_tag() aborts object loading and falsely blames an unrelated program if any invalid use of __load_dynamic exists in the BTF.
- [Medium] The __load_dynamic BTF tag is silently ignored for programs with unrecognized or custom ELF section names.
- [Low] Multi-line comment for __load_dynamic in bpf_helpers.h violates the strict BPF subsystem comment style.
--
commit 2d3fcf27d117a0a3d873c0c1212bb77a4447fc91
Author: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
libbpf: Support declarative dynamic load via BTF decl_tag
This commit adds a loadtype:dynamic BTF decl_tag, exposed as __load_dynamic
in bpf_helpers.h, letting a program declare itself dynamically-loaded in
its source instead of requiring an imperative call. The tag is checked
in the existing per-program loop in bpf_object_init_progs().
> diff --git a/tools/lib/bpf/bpf_helpers.h b/tools/lib/bpf/bpf_helpers.h
> index 9d160b5b9c0ee..52978f0e04539 100644
> --- a/tools/lib/bpf/bpf_helpers.h
> +++ b/tools/lib/bpf/bpf_helpers.h
> @@ -218,6 +218,11 @@ enum libbpf_tristate {
> #define __arg_untrusted __attribute((btf_decl_tag("arg:untrusted")))
> #define __arg_arena __attribute((btf_decl_tag("arg:arena")))
>
> +/* Mark a BPF program to be loaded dynamically instead of autoloaded,
> + * equivalent to bpf_program__set_load_type(prog, BPF_PROG_LOAD_TYPE_DYNAMIC).
> + */
[Severity: Low]
This isn't a bug, but does this multi-line comment style violate the BPF
subsystem rules? The BPF subsystem coding style guide requires multi-line
comments to have the opening `/*` on its own line.
> +#define __load_dynamic __attribute__((btf_decl_tag("loadtype:dynamic")))
> +
> #ifndef ___bpf_concat
> #define ___bpf_concat(a, b) a ## b
> #endif
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index cd1d00c47841b..5d58dbf25234f 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -7156,6 +7158,50 @@ static int bpf_prog_assign_exc_cb(struct bpf_object *obj, struct bpf_program *pr
> return 0;
> }
>
> +#define DYNLOAD_DECL_TAG "loadtype:dynamic"
> +
> +/*
> + * Check whether a BPF program is annotated with the DYNLOAD_DECL_TAG BTF
> + * decl tag, requesting that it be loaded dynamically after the object load.
> + *
> + * Returns 1 if the tag is present, 0 if it is not, negative error otherwise.
> + */
> +static int prog_has_dynload_tag(struct bpf_object *obj, struct bpf_program *prog)
> +{
> + int i, n;
> +
> + if (!obj->btf)
> + return 0;
> +
> + n = btf__type_cnt(obj->btf);
> + for (i = 1; i < n; i++) {
> + const struct btf_type *tag_t, *fn_t;
> +
> + tag_t = btf__type_by_id(obj->btf, i);
> + if (!btf_is_decl_tag(tag_t) || btf_decl_tag(tag_t)->component_idx != -1)
> + continue;
> + if (strcmp(btf__str_by_offset(obj->btf, tag_t->name_off),
> + DYNLOAD_DECL_TAG) != 0)
> + continue;
> +
> + /* a decl tag's type field points at the entity it decorates */
> + fn_t = btf__type_by_id(obj->btf, tag_t->type);
> + if (!btf_is_func(fn_t) || btf_func_linkage(fn_t) != BTF_FUNC_GLOBAL) {
> + pr_warn("prog '%s': " DYNLOAD_DECL_TAG
> + " decl tag must be applied to a global function\n",
> + prog->name);
> + return -EINVAL;
> + }
> +
> + if (strcmp(btf__str_by_offset(obj->btf, fn_t->name_off), prog->name) != 0)
> + continue; /* tag belongs to a different program */
[Severity: Medium]
Will prog_has_dynload_tag() abort object initialization and falsely blame the
current program if an invalid use of the __load_dynamic tag exists elsewhere
in the BTF?
Because the function linkage check is performed before verifying if the tag
actually belongs to the current program being processed, it appears the first
program processed will trigger the failure.
> +
> + return 1;
> + }
> +
> + return 0;
> +}
> +
[ ... ]
> @@ -8467,6 +8516,22 @@ static int bpf_object_init_progs(struct bpf_object *obj, const struct bpf_object
> return err;
> }
> }
[Severity: Medium]
Is the __load_dynamic BTF tag silently ignored for programs with unrecognized
or custom ELF section names?
Looking slightly earlier in bpf_object_init_progs(), there is a check:
prog->sec_def = find_sec_def(prog->sec_name);
if (!prog->sec_def) {
/* couldn't guess, but user might manually specify */
pr_debug("prog '%s': unrecognized ELF section name '%s'\n",
prog->name, prog->sec_name);
continue;
}
Because the tag parsing added here is placed after this continue statement,
it seems the declarative dynamic load feature will be bypassed for programs
with custom section names.
> +
> + if (prog_is_subprog(obj, prog))
> + continue;
> +
> + err = prog_has_dynload_tag(obj, prog);
> + if (err < 0)
> + return err;
> + if (err > 0) {
> + err = bpf_program__set_load_type(prog, BPF_PROG_LOAD_TYPE_DYNAMIC);
> + if (err) {
> + pr_warn("prog '%s': failed to apply " DYNLOAD_DECL_TAG
> + " decl tag: %s\n",
> + prog->name, errstr(err));
> + return err;
> + }
> + }
> }
>
> return 0;
[Severity: High]
Does applying __load_dynamic to programs requiring BTF target resolution cause
object opening to unconditionally fail?
When bpf_object_init_progs() calls bpf_program__set_load_type(), which routes
through bpf_program__set_dynamicload(), it hits this check:
long flags = prog->sec_def ?
(long)prog->sec_def->cookie : SEC_ATTACH_BTF;
if ((flags & SEC_ATTACH_BTF) && !prog->attach_btf_id)
return libbpf_err(-EINVAL);
Since the declarative tag is processed during early object initialization, long
before attach_btf_id is resolved by libbpf or can be set by the user,
attach_btf_id is always zero here. Doesn't this cause the object open phase to
unconditionally abort with -EINVAL?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826181825.1826541-1-andrey.grodzovsky@crowdstrike.com?part=4
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 5/5] selftests/bpf: Cover BPF program dynamic loading
2026-08-26 18:18 ` [PATCH bpf-next v2 5/5] selftests/bpf: Cover BPF program dynamic loading Andrey Grodzovsky
@ 2026-08-26 18:33 ` sashiko-bot
2026-08-26 19:25 ` bot+bpf-ci
2026-09-11 23:44 ` Andrii Nakryiko
2 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-08-26 18:33 UTC (permalink / raw)
To: Andrey Grodzovsky; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Calling `bpf_program__load_dynamically()` multiple times on the same program overwrites `prog->fd` and leaks the previously loaded BPF program's file descriptor.
- [Medium] bpf_link leaked on error paths leaves system-wide raw tracepoint active.
- [Low] Multi-line comments do not follow the mandatory BPF subsystem style.
- [Low] `dynamicload_verify_override()` is skipped entirely if the preceding `test_dynamicload` logic encounters a failure.
- [Low] Missing early return check for state-altering assertion allows test execution to continue in an invalid state.
--
commit eb781cf99ff72cd5528638c785d25a5ff470c1c9
Author: Slava Imameev <slava.imameev@crowdstrike.com>
selftests/bpf: Cover BPF program dynamic loading
This commit adds load_type and dynamicload tests to verify the per-program load
type enum (disabled/auto/dynamic) and its transitions. It also tests the full
lifecycle of dynamic load/attach/reattach/unload.
> diff --git a/tools/testing/selftests/bpf/prog_tests/dynamicload.c b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
> new file mode 100644
> index 0000000000000..799c43034ff92
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
> @@ -0,0 +1,222 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +#include <test_progs.h>
> +#include <time.h>
> +#include "test_dynamicload.skel.h"
> +
> +/* prog4 is tagged __load_dynamic in the source instead of being set
> + * imperatively; verify that an explicit bpf_program__set_load_type() call
> + * before load overrides the tag's declarative default.
> + */
[Severity: Low]
This isn't a bug, but this multi-line comment does not follow the mandatory BPF
subsystem style, which requires the opening '/*' to be on its own line.
> +static void dynamicload_verify_override(void)
> +{
[ ... ]
> +void test_dynamicload(void)
> +{
> + struct bpf_link *link;
> + struct test_dynamicload *skel;
> + int err;
> +
> + skel = test_dynamicload__open();
> + if (!ASSERT_OK_PTR(skel, "skel_open"))
> + return;
> +
> + /* the __load_dynamic tag alone, with no imperative call, must set
> + * prog4's load type before it is ever touched below
> + */
[Severity: Low]
This isn't a bug, but this multi-line comment also does not follow the
mandatory BPF subsystem style.
[ ... ]
> + /* attach prog3 */
> + link = bpf_program__attach(skel->progs.prog3);
> + if (!ASSERT_OK_PTR(link, "attach"))
> + goto cleanup;
> +
> + usleep(1);
> +
> + if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called"))
> + goto cleanup;
[Severity: Medium]
Does this code leak the manually managed link object and leave the system-wide
raw tracepoint active if this assertion fails? The cleanup path only destroys
the skeleton, which doesn't track this standalone bpf_link object.
[ ... ]
> + err = bpf_program__unload_dynamically(skel->progs.prog4);
> + ASSERT_OK(err, "prog4_unload_dynamically");
> +
> + test_dynamicload__destroy(skel);
> +
> + /* separate scenario: imperative override of the declarative tag */
> + dynamicload_verify_override();
> + return;
> +
> +cleanup:
> + test_dynamicload__destroy(skel);
> +}
[Severity: Low]
Is it intentional that dynamicload_verify_override() is skipped entirely if the
preceding test_dynamicload() logic encounters a failure and jumps to cleanup?
This could mask test coverage since it won't be reported as executed or skipped.
> 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..fc91f6666c506
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/load_type.c
> @@ -0,0 +1,167 @@
[ ... ]
> + /* bpf_program__set_autoload() is a thin forwarder to set_load_type(),
> + * restricted to AUTO/DISABLED to preserve its original bool on/off
> + * meaning; it does change the load type of a program that isn't
> + * currently BPF_PROG_LOAD_TYPE_AUTO.
> + */
[Severity: Low]
This isn't a bug, but this multi-line comment does not follow the mandatory BPF
subsystem style, which requires the opening '/*' to be on its own line.
[ ... ]
> + /* loaded program type cannot be changed */
> + err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DISABLED);
> + ASSERT_ERR(err, "set_load_type_after_load");
> +
> + err = test_load_type__attach(skel);
[Severity: Low]
Should there be an early return check here? Without it, if the underlying API
mistakenly succeeds, the program's load state is incorrectly modified to
DISABLED and the test continues running, which could cause cascading and
confusing failures.
[ ... ]
> + err = bpf_program__load_dynamically(skel->progs.prog3, 0);
> + if (!ASSERT_OK(err, "load_dynamically_1"))
> + goto cleanup;
> +
> + err = bpf_program__load_dynamically(skel->progs.prog3, 0);
> + if (!ASSERT_OK(err, "load_dynamically_2"))
> + goto cleanup;
[Severity: High]
Does calling bpf_program__load_dynamically() multiple times on the same program
overwrite prog->fd and leak the previously loaded BPF program's file descriptor?
The test seems to actively rely on this behavior by loading the program twice
without an intervening unload.
> +
> + /* attach prog3 */
> + link = bpf_program__attach(skel->progs.prog3);
> + if (!ASSERT_OK_PTR(link, "attach"))
> + goto cleanup;
> +
> + usleep(1);
> +
> + if (!ASSERT_TRUE(skel->bss->prog3_called, "prog3_called_again"))
> + goto cleanup;
[Severity: Medium]
Will this bypass bpf_link__destroy(link) and leak the system-wide tracepoint
attachment if the assertion fails and jumps to cleanup?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826181825.1826541-1-andrey.grodzovsky@crowdstrike.com?part=5
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 3/5] libbpf: Convert bpf_program__set_autoload() to load-type enum
2026-08-26 18:18 ` [PATCH bpf-next v2 3/5] libbpf: Convert bpf_program__set_autoload() to load-type enum Andrey Grodzovsky
@ 2026-08-26 18:34 ` sashiko-bot
2026-08-26 19:25 ` bot+bpf-ci
2026-09-11 23:44 ` Andrii Nakryiko
2 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-08-26 18:34 UTC (permalink / raw)
To: Andrey Grodzovsky; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] Modifying the signature of the public API `bpf_program__set_autoload` from `bool` to `enum bpf_prog_load_type` breaks ABI and C++ API compatibility.
- [Medium] Internal static function `bpf_program__set_dynamicload` incorrectly uses the public `libbpf_err()` wrapper, causing public APIs to omit it on their return paths.
--
commit 21a83792a849333b99398b408870c76df6690254
Author: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
libbpf: Convert bpf_program__set_autoload() to load-type enum
This commit converts bpf_program__set_autoload() from bool to enum
bpf_prog_load_type to unify the API. It also restores autoattach when
leaving the dynamic load type.
> diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
> --- a/tools/lib/bpf/libbpf.h
> +++ b/tools/lib/bpf/libbpf.h
[ ... ]
> @@ -376,7 +389,18 @@ LIBBPF_API void bpf_program__set_ifindex(struct bpf_program *prog,
> LIBBPF_API const char *bpf_program__name(const struct bpf_program *prog);
> LIBBPF_API const char *bpf_program__section_name(const struct bpf_program *prog);
> LIBBPF_API bool bpf_program__autoload(const struct bpf_program *prog);
> -LIBBPF_API int bpf_program__set_autoload(struct bpf_program *prog, bool autoload);
> +/**
> + * @brief **bpf_program__set_autoload()** is retained for backwards
> + * compatibility; **bpf_program__set_load_type()** is the preferred API.
> + * The enum's numbering (DISABLED=0, AUTO=1, DYNAMIC=2) matches the previous
> + * bool semantics (false=0, true=1), so existing bool callers keep compiling
> + * and behaving unchanged. Only BPF_PROG_LOAD_TYPE_AUTO and
> + * BPF_PROG_LOAD_TYPE_DISABLED are accepted, preserving the original
> + * autoload on/off meaning; use bpf_program__set_load_type() to set
> + * BPF_PROG_LOAD_TYPE_DYNAMIC.
> + */
> +LIBBPF_API int bpf_program__set_autoload(struct bpf_program *prog,
> + enum bpf_prog_load_type autoload);
[Severity: Critical]
Does this signature change introduce a regression for existing compiled
binaries and C++ consumers?
Existing binaries compiled with the old bool signature pass a 1-byte value
in a register. The new library expects a 32-bit enum, which might read
undefined garbage from the upper 24 bits of the lower 32-bit register half.
This garbage could cause the validation in bpf_program__set_autoload() to
falsely trigger and return -EINVAL:
tools/lib/bpf/libbpf.c:bpf_program__set_autoload() {
if (autoload != BPF_PROG_LOAD_TYPE_AUTO &&
autoload != BPF_PROG_LOAD_TYPE_DISABLED)
return libbpf_err(-EINVAL);
...
}
Furthermore, C++ code calling bpf_program__set_autoload(prog, true) will
fail to compile because C++ does not allow implicit conversion from bool
to an unscoped enum type.
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -15295,15 +15289,28 @@ static int bpf_program__set_dynamicload(struct bpf_program *prog)
>
> int bpf_program__set_load_type(struct bpf_program *prog, enum bpf_prog_load_type type)
> {
> + if (!prog)
> + return libbpf_err(-EINVAL);
> +
> if (prog->obj->state >= OBJ_LOADED)
> return libbpf_err(-EINVAL);
>
> switch (type) {
> case BPF_PROG_LOAD_TYPE_DYNAMIC:
> return bpf_program__set_dynamicload(prog);
[Severity: Medium]
Does this propagate an already-wrapped error code rather than applying the
wrapper at the public boundary?
The libbpf error handling convention mandates that errno must be set exactly
at the public API boundary, and internal functions like
bpf_program__set_dynamicload() must return raw negative error codes.
Because bpf_program__set_dynamicload() incorrectly calls libbpf_err()
internally, public APIs like bpf_program__set_load_type() and
bpf_program__set_autoload() end up propagating this wrapped error code
directly without applying the wrapper on their own return paths.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826181825.1826541-1-andrey.grodzovsky@crowdstrike.com?part=3
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 3/5] libbpf: Convert bpf_program__set_autoload() to load-type enum
2026-08-26 18:18 ` [PATCH bpf-next v2 3/5] libbpf: Convert bpf_program__set_autoload() to load-type enum Andrey Grodzovsky
2026-08-26 18:34 ` sashiko-bot
@ 2026-08-26 19:25 ` bot+bpf-ci
2026-09-11 23:44 ` Andrii Nakryiko
2 siblings, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-08-26 19:25 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: martin.kelly, slava.imameev, linux-open-source, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 5527 bytes --]
> commit 677f95c8adc5274970774bb635eca58cda6397dc
> Author: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
>
> libbpf: Convert bpf_program__set_autoload() to load-type enum
>
> Convert bpf_program__set_autoload() from bool to enum bpf_prog_load_type,
> to unify the API with the new tri-state load type. Reject
> BPF_PROG_LOAD_TYPE_DYNAMIC to preserve the original autoload on/off
> meaning; only BPF_PROG_LOAD_TYPE_DYNAMIC is dynamic-load-only. Also
> restore autoattach when leaving BPF_PROG_LOAD_TYPE_DYNAMIC via
> set_load_type(), fixing a bug where a program left dynamic load and
> became autoload-eligible but never re-attached.
>
> Assisted-by: Claude:claude-sonnet-5
> Suggested-by: Andrii Nakryiko <andrii@kernel.org>
> Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
This looks like a bug fix for stack argument support. Should this
include:
Fixes: d8626c4dc022 ("libbpf: BPF programs dynamic loading and attaching")
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index c3fe1de432108..cd1d00c47841b 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -15295,15 +15289,28 @@ static int bpf_program__set_dynamicload(struct bpf_program *prog)
>
> int bpf_program__set_load_type(struct bpf_program *prog, enum bpf_prog_load_type type)
> {
> + if (!prog)
> + return libbpf_err(-EINVAL);
> +
> if (prog->obj->state >= OBJ_LOADED)
> return libbpf_err(-EINVAL);
>
> switch (type) {
> case BPF_PROG_LOAD_TYPE_DYNAMIC:
> return bpf_program__set_dynamicload(prog);
> - default:
> + case BPF_PROG_LOAD_TYPE_AUTO:
> + case BPF_PROG_LOAD_TYPE_DISABLED:
> + /*
> + * Leaving the dynamic state: set_dynamicload() cleared
> + * autoattach because dynamically loaded programs are attached
> + * explicitly. Restore the default for the target load type.
> + */
> + if (prog->load_type == BPF_PROG_LOAD_TYPE_DYNAMIC)
> + prog->autoattach = true;
> prog->load_type = type;
> break;
> + default:
> + return libbpf_err(-EINVAL);
> }
>
> return 0;
Does this unconditional restore overwrite an explicit user setting?
autoattach is a user-visible knob (bpf_program__set_autoattach() at
libbpf.c:9830) whose default true is set once in
bpf_object__init_prog(). Because the code cannot distinguish 'cleared
by set_dynamicload()' from 'cleared by the user', this sequence
silently re-enables auto-attach:
bpf_program__set_autoattach(prog, false);
bpf_program__set_load_type(prog, BPF_PROG_LOAD_TYPE_DYNAMIC);
bpf_program__set_load_type(prog, BPF_PROG_LOAD_TYPE_AUTO);
After this, bpf_object__attach_skeleton() will attach the program
during skel__attach() even though the user asked for it not to be
attached. For a tc/XDP/cgroup program that is a live behavioural
change. The same holds if set_autoattach(false) is called while the
program is DYNAMIC and the load type is later moved to AUTO or
DISABLED.
Would remembering the pre-DYNAMIC value or only restoring when
set_dynamicload() actually cleared it preserve the intended fix without
overriding the caller?
> diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
> index 54d7558612893..a56068f0f1122 100644
> --- a/tools/lib/bpf/libbpf.h
> +++ b/tools/lib/bpf/libbpf.h
[ ... ]
> @@ -376,7 +389,18 @@ LIBBPF_API void bpf_program__set_ifindex(struct bpf_program *prog,
> LIBBPF_API const char *bpf_program__name(const struct bpf_program *prog);
> LIBBPF_API const char *bpf_program__section_name(const struct bpf_program *prog);
> LIBBPF_API bool bpf_program__autoload(const struct bpf_program *prog);
> -LIBBPF_API int bpf_program__set_autoload(struct bpf_program *prog, bool autoload);
> +/**
> + * @brief **bpf_program__set_autoload()** is retained for backwards
> + * compatibility; **bpf_program__set_load_type()** is the preferred API.
> + * The enum's numbering (DISABLED=0, AUTO=1, DYNAMIC=2) matches the previous
> + * bool semantics (false=0, true=1), so existing bool callers keep compiling
> + * and behaving unchanged. Only BPF_PROG_LOAD_TYPE_AUTO and
> + * BPF_PROG_LOAD_TYPE_DISABLED are accepted, preserving the original
> + * autoload on/off meaning; use bpf_program__set_load_type() to set
> + * BPF_PROG_LOAD_TYPE_DYNAMIC.
> + */
> +LIBBPF_API int bpf_program__set_autoload(struct bpf_program *prog,
> + enum bpf_prog_load_type autoload);
> LIBBPF_API bool bpf_program__autoattach(const struct bpf_program *prog);
> LIBBPF_API void bpf_program__set_autoattach(struct bpf_program *prog, bool autoattach);
Could this follow libbpf's API documentation convention? Per
Documentation/bpf/libbpf/libbpf_naming_convention.rst, the convention
expects 'Parameters are denoted with the @param directive, there should
be one for each parameter. If this is a function with a non-void return,
use the @return directive to document it.'
The block has neither @param prog / @param autoload nor @return, even
though the function takes two parameters and returns a non-void int
whose new error condition (-EINVAL for anything other than AUTO/DISABLED)
is precisely the part a caller needs documented. Compare
bpf_map__set_autoattach() a few hundred lines below (libbpf.h:1164-1171),
which uses @brief/@param/@return as the convention requires.
---
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/33000627295
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 1/5] libbpf: BPF program load type enum
2026-08-26 18:18 ` [PATCH bpf-next v2 1/5] libbpf: BPF program load type enum Andrey Grodzovsky
@ 2026-08-26 19:25 ` bot+bpf-ci
2026-09-11 23:43 ` Andrii Nakryiko
1 sibling, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-08-26 19:25 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: martin.kelly, slava.imameev, linux-open-source, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 4813 bytes --]
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index b749c01742ee0..821d854d43a9f 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -15221,3 +15219,17 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
> free(s->progs);
> free(s);
> }
> +
> +int bpf_program__set_load_type(struct bpf_program *prog, enum bpf_prog_load_type type)
> +{
> + if (prog->obj->state >= OBJ_LOADED)
> + return libbpf_err(-EINVAL);
> +
> + prog->load_type = type;
> + return 0;
> +}
> +
> +enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog)
> +{
> + return prog->load_type;
> +}
The new exported setter stores type without validating that it is one of
the two values enum bpf_prog_load_type defines. After this patch, the code
that checks whether to apply relocations tests load_type !=
BPF_PROG_LOAD_TYPE_DISABLED in multiple places:
obj_needs_vmlinux_btf()
bpf_object__relocate_core()
bpf_object__relocate() [both call and data relocation sections]
while the code that actually loads the program tests load_type !=
BPF_PROG_LOAD_TYPE_AUTO:
bpf_object__load_progs()
bpf_object__attach_skeleton()
The previous bool autoload field made an out-of-domain value impossible, so
all sites agreed by construction. After this patch,
bpf_program__set_load_type(prog, 2) returns 0 and produces a state the two
halves of libbpf disagree about: vmlinux BTF is loaded and CO-RE, call, and
data relocations are applied for the program, but bpf_object__load_progs()
silently skips it (prog->fd stays -1) and bpf_object__attach_skeleton()
silently skips it too. The application gets no error from any libbpf call -
bpf_object__load() returns success - and cannot recover the state through
the bool API, since bpf_program__autoload() reports false while
bpf_program__load_type() reports the bogus value.
This is reachable from outside the tree because the symbol is exported in
this same commit (libbpf.map LIBBPF_1.8.0) with a two-value enum, and value
2 (BPF_PROG_LOAD_TYPE_DYNAMIC) is added later in the same dev cycle without
an ABI version bump, so an application built against the newer header links
successfully against a library built at this commit and gets a silently
half-prepared program instead of an error.
I see that commit 677f95c8adc5274970774bb635eca58cda6397dc later in the
series adds "default: return libbpf_err(-EINVAL);" to the switch in the
converted bpf_program__set_autoload(), which would fix this. Could that
validation be added here in the initial setter?
> diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
> index b965ad5715403..700634664e326 100644
> --- a/tools/lib/bpf/libbpf.h
> +++ b/tools/lib/bpf/libbpf.h
> @@ -2078,6 +2078,21 @@ 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 type:
> + *
> + * - BPF_PROG_LOAD_TYPE_DISABLED: the program is not loaded.
> + * - BPF_PROG_LOAD_TYPE_AUTO: the program is autoloaded when the bpf_object is loaded.
> + */
> +enum bpf_prog_load_type {
> + BPF_PROG_LOAD_TYPE_DISABLED = 0,
> + BPF_PROG_LOAD_TYPE_AUTO,
> +};
> +
> +LIBBPF_API int bpf_program__set_load_type(struct bpf_program *prog,
> + enum bpf_prog_load_type loadtype);
> +LIBBPF_API enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog);
> +
> #ifdef __cplusplus
> } /* extern "C" */
> #endif
Two new LIBBPF_API symbols are being added with no API documentation. The
comment block uses the doxygen marker /** without the required directives.
Documentation/bpf/libbpf/libbpf_naming_convention.rst states that API
documentation must start with @brief and include @param for each parameter
and @return for non-void returns. The immediately preceding declaration,
bpf_program__clone(), follows this convention.
The missing documentation matters for behaviour a caller cannot infer:
bpf_program__set_load_type() returns -EINVAL once obj->state >= OBJ_LOADED,
and bpf_program__load_type() has no way to report an error. There is also a
small parameter-name mismatch: the declaration names the argument loadtype
while the definition in libbpf.c names it type.
I see that commit 677f95c8adc5274970774bb635eca58cda6397dc changes the
block's marker from /** to /* (acknowledging it is not a doxygen API
comment) and relocates the enum, but the two new functions remain
undocumented there as well. Could API documentation be added for these new
exported functions?
---
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/33000627295
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 5/5] selftests/bpf: Cover BPF program dynamic loading
2026-08-26 18:18 ` [PATCH bpf-next v2 5/5] selftests/bpf: Cover BPF program dynamic loading Andrey Grodzovsky
2026-08-26 18:33 ` sashiko-bot
@ 2026-08-26 19:25 ` bot+bpf-ci
2026-09-11 23:44 ` Andrii Nakryiko
2 siblings, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-08-26 19:25 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: martin.kelly, slava.imameev, linux-open-source, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 10803 bytes --]
> diff --git a/tools/testing/selftests/bpf/prog_tests/dynamicload.c b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
> new file mode 100644
> index 0000000000000..799c43034ff92
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
[ ... ]
> +static void dynamicload_verify_override(void)
> +{
> + struct test_dynamicload *skel;
> + int err;
> +
> + skel = test_dynamicload__open();
> + if (!ASSERT_OK_PTR(skel, "skel_open"))
> + return;
> +
> + err = bpf_program__set_load_type(skel->progs.prog4, BPF_PROG_LOAD_TYPE_DISABLED);
> + if (!ASSERT_OK(err, "set_load_type_disabled"))
> + goto cleanup;
> +
> + if (!ASSERT_EQ(bpf_program__load_type(skel->progs.prog4), BPF_PROG_LOAD_TYPE_DISABLED,
> + "prog4_load_type_overridden"))
> + goto cleanup;
> +
> + /* keep the other dynamic-load-only programs out of the way of this load */
> + bpf_program__set_load_type(skel->progs.prog1, BPF_PROG_LOAD_TYPE_DISABLED);
> + bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DISABLED);
The comment says prog1 and prog3 are "dynamic-load-only programs" that
need to be kept out of the way, but in test_dynamicload.c only prog4
has the __load_dynamic tag. prog1 and prog3 are plain
SEC("raw_tp/sys_enter") programs, so bpf_object__init_prog() assigns
load_type = BPF_PROG_LOAD_TYPE_AUTO to both. Without these calls they
would autoload alongside prog2, but that wouldn't affect the prog4
override assertion. Does the comment need updating?
Also, these two bpf_program__set_load_type() calls discard the return
value while all other calls in this function check them. Is the
inconsistency intentional?
> +
> + err = test_dynamicload__load(skel);
> + if (!ASSERT_OK(err, "skel_load"))
> + goto cleanup;
> +
> + /* prog4 was never loaded, so it cannot be loaded dynamically either */
> + err = bpf_program__load_dynamically(skel->progs.prog4, 0);
> + ASSERT_ERR(err, "load_dynamically_after_override");
[ ... ]
> + /* reloading prog3 must fail as it was unloaded as a regular program */
> + err = bpf_program__load_dynamically(skel->progs.prog3, 0);
> + ASSERT_ERR(err, "load_dynamically_after_regular_unload");
Does this assertion test the rule the comment describes? Looking at
bpf_program__load_dynamically() in libbpf.c (d8626c4dc022c), it only
rejects three things: obj->state < OBJ_LOADED, prog_is_subprog(), and
prog->load_type != DYNAMIC. After line 175's bpf_program__unload(),
obj->state is still OBJ_LOADED and prog3->load_type is still DYNAMIC,
so both gates pass.
bpf_program__unload() (libbpf.c:799) doesn't touch prog->load_type:
void bpf_program__unload(struct bpf_program *prog)
{
if (!prog)
return;
zclose(prog->fd);
zfree(&prog->func_info);
zfree(&prog->line_info);
zfree(&prog->subprogs);
}
The reload only fails because zfree() NULLs prog->func_info but leaves
prog->func_info_cnt non-zero. bpf_object_load_prog() then copies that
inconsistent pair into the load attrs:
if (obj->btf && btf__fd(obj->btf) >= 0 && kernel_supports(obj, FEAT_BTF_FUNC)) {
load_attr.prog_btf_fd = btf__fd(obj->btf);
load_attr.func_info = prog->func_info; /* NULL */
load_attr.func_info_rec_size = prog->func_info_rec_size;
load_attr.func_info_cnt = prog->func_info_cnt; /* still 1 */
and BPF_PROG_LOAD fails with -EFAULT in check_btf_func()'s
copy_from_user(). So the test is really asserting that the kernel
rejects NULL func_info with non-zero func_info_cnt, not that a
regularly-unloaded program cannot be dynamically reloaded.
Two consequences: (1) the assertion is environment-dependent. The
func_info block above is conditional on obj->btf having a valid kernel
fd and FEAT_BTF_FUNC being supported, and prog->func_info_cnt is only
set when obj->btf_ext exists. If BTF fails to load or .BTF.ext is
absent, func_info_cnt is 0, the reload succeeds, and ASSERT_ERR()
fails. The test then reports a kernel regression that doesn't exist.
(2) Zeroing func_info_cnt/line_info_cnt in bpf_program__unload() is
the natural fix for the NULL-pointer/non-zero-count inconsistency;
doing so makes this reload succeed and breaks the test.
Should the series implement the rule the comment describes, perhaps by
having bpf_program__unload() reset prog->load_type, or by adding an
explicit 'unloaded, data released' state that
bpf_program__load_dynamically() checks? Otherwise this assertion and
comment should be dropped.
> +
> + /* run prog4 (declaratively tagged) through the same dynamic
> + * load/attach/trigger/detach/unload cycle as prog3
> + */
> + err = bpf_program__load_dynamically(skel->progs.prog4, 0);
> + if (!ASSERT_OK(err, "prog4_load_dynamically"))
> + goto cleanup;
> +
> + link = bpf_program__attach(skel->progs.prog4);
> + if (!ASSERT_OK_PTR(link, "prog4_attach"))
> + goto cleanup;
> +
> + usleep(1);
> +
> + if (!ASSERT_TRUE(skel->bss->prog4_called, "prog4_called"))
> + goto cleanup;
> +
> + err = bpf_link__destroy(link);
> + if (!ASSERT_OK(err, "prog4_link_destroy"))
> + goto cleanup;
> +
> + err = bpf_program__unload_dynamically(skel->progs.prog4);
> + ASSERT_OK(err, "prog4_unload_dynamically");
> +
> + test_dynamicload__destroy(skel);
> +
> + /* separate scenario: imperative override of the declarative tag */
> + dynamicload_verify_override();
> + return;
> +
> +cleanup:
> + test_dynamicload__destroy(skel);
> +}
dynamicload_verify_override() only runs on the fully-successful path.
test_dynamicload() has 21 'goto cleanup' exits (lines 59, 61, 74, 78,
91, 96, 101, 106, 121, 126, 131, 136, 150, 155, 160, 165, 170, 195,
199, 204, 208), and every one jumps past dynamicload_verify_override()
to the cleanup label. So any single failure anywhere in the prog3
lifecycle silently removes coverage for the declarative-tag override
behaviour that commit 5bddcc170b310 introduced, the scenario the
comment itself calls "separate".
The two scenarios are independent: dynamicload_verify_override() opens
its own skeleton and shares no state with the caller. Would
test__start_subtest() let each scenario have its own pass/fail line in
the test_progs output instead of hiding one behind the other's success?
> 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..fc91f6666c506
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/load_type.c
[ ... ]
> + /* leaving DYNAMIC for AUTO must restore autoattach (regression test for
> + * the autoattach residue bug: set_dynamicload() clears autoattach, and
> + * nothing used to restore it on exit)
> + */
> + err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_AUTO);
> + if (!ASSERT_OK(err, "set_load_type_auto"))
> + goto cleanup;
> +
> + if (!ASSERT_EQ(bpf_program__load_type(skel->progs.prog3), BPF_PROG_LOAD_TYPE_AUTO,
> + "prog3_load_type_auto"))
> + goto cleanup;
> +
> + if (!ASSERT_TRUE(bpf_program__autoattach(skel->progs.prog3), "prog3_autoattach_restored"))
> + goto cleanup;
> +
> + /* the same residue can relay through DISABLED and resurface on a later
> + * DISABLED -> AUTO transition, so the fix must cover both exit edges
> + */
> + err = bpf_program__set_load_type(skel->progs.prog3, BPF_PROG_LOAD_TYPE_DYNAMIC);
Does this comment match the code? The implementation in 677f95c8adc52
restores autoattach with a single guard keyed off the source state:
case BPF_PROG_LOAD_TYPE_AUTO:
case BPF_PROG_LOAD_TYPE_DISABLED:
if (prog->load_type == BPF_PROG_LOAD_TYPE_DYNAMIC)
prog->autoattach = true;
So the DYNAMIC -> DISABLED step already clears the residue; there is
nothing left to "relay through DISABLED", and the subsequent DISABLED ->
AUTO step cannot "resurface" it because prog->load_type is no longer
DYNAMIC. There is also no second "exit edge" for the fix to cover: AUTO
and DISABLED share one fall-through arm.
Without the fix, the assertion at line 102 would fail for the same
single reason as line 84 (autoattach never restored on leaving DYNAMIC),
not because of a distinct DISABLED->AUTO path. Does the comment claim
coverage of a code path that doesn't exist?
[ ... ]
> + err = bpf_program__load_dynamically(skel->progs.prog3, 0);
> + if (!ASSERT_OK(err, "load_dynamically_1"))
> + goto cleanup;
> +
> + err = bpf_program__load_dynamically(skel->progs.prog3, 0);
> + if (!ASSERT_OK(err, "load_dynamically_2"))
> + goto cleanup;
The test calls bpf_program__load_dynamically(prog3, 0) twice in a row
with no intervening bpf_program__unload_dynamically() and asserts both
calls succeed, but there is no comment saying what the second call is
meant to prove.
In libbpf (d8626c4dc022c) bpf_program__load_dynamically() unconditionally
calls bpf_object_load_prog(obj, prog, ..., &prog->fd), and
bpf_object_load_prog() ends with a bare *prog_fd = ret; -- it never
inspects or closes an existing prog->fd. So the second call creates a
second kernel BPF program and overwrites prog->fd, leaking the first fd
and pinning the first program instance in the kernel for the remaining
lifetime of the test_progs process. bpf_object__close() only closes the
current prog->fd.
Peer test prog_tests/dynamicload.c does the reload case correctly
(unload_dynamically() then load_dynamically(), lines 148-155), which
makes the unqualified double load here look like a copy/paste rather
than intentional coverage. Does the second call need to be preceded by
bpf_program__unload_dynamically(), or should it be expected to fail? If
the assertion is intentional, it codifies "reload without unload silently
leaks the previous program" as expected API behaviour.
[ ... ]
Andrii Nakryiko raised fundamental concerns in v1 about the complexity
of adding a third autoload state (dynamic) to libbpf. From the review
at https://lore.kernel.org/bpf/CAEf4BzYWe0KCzA4-qwAGp5n_ydJ0_zyLSO=Crr_vewFHzZ0t6Q@mail.gmail.com/:
"I don't want to complicate bpf_object internals with this third
autoload state, as this creates tons of non-obvious gotchas that
have to constantly be checked whenever any new feature is added to
bpf_object (or even during refactorings)."
He proposed alternative approaches: (1) splitting BPF objects with map
sharing, or (2) a new bpf_object__prepare() API to split load into
prepare/load steps. The patch series proceeded with the dynamic loading
approach without addressing these architectural concerns. Are the
alternative approaches not viable for the use case, or is there a plan
to revisit the design?
---
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/33000627295
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 4/5] libbpf: Support declarative dynamic load via BTF decl_tag
2026-08-26 18:18 ` [PATCH bpf-next v2 4/5] libbpf: Support declarative dynamic load via BTF decl_tag Andrey Grodzovsky
2026-08-26 18:33 ` sashiko-bot
@ 2026-08-26 19:25 ` bot+bpf-ci
1 sibling, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-08-26 19:25 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: martin.kelly, slava.imameev, linux-open-source, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 5956 bytes --]
> diff --git a/tools/lib/bpf/bpf_helpers.h b/tools/lib/bpf/bpf_helpers.h
> index 9d160b5b9c0ee..52978f0e04539 100644
> --- a/tools/lib/bpf/bpf_helpers.h
> +++ b/tools/lib/bpf/bpf_helpers.h
> @@ -218,6 +218,11 @@ enum libbpf_tristate {
> #define __arg_untrusted __attribute((btf_decl_tag("arg:untrusted")))
> #define __arg_arena __attribute((btf_decl_tag("arg:arena")))
>
> +/* Mark a BPF program to be loaded dynamically instead of autoloaded,
> + * equivalent to bpf_program__set_load_type(prog, BPF_PROG_LOAD_TYPE_DYNAMIC).
> + */
> +#define __load_dynamic __attribute__((btf_decl_tag("loadtype:dynamic")))
> +
> #ifndef ___bpf_concat
> #define ___bpf_concat(a, b) a ## b
> #endif
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index cd1d00c47841b..5d58dbf25234f 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> @@ -7156,6 +7158,50 @@ static int bpf_prog_assign_exc_cb(struct bpf_object *obj, struct bpf_program *pr
> return 0;
> }
>
> +#define DYNLOAD_DECL_TAG "loadtype:dynamic"
> +
> +/*
> + * Check whether a BPF program is annotated with the DYNLOAD_DECL_TAG BTF
> + * decl tag, requesting that it be loaded dynamically after the object load.
> + *
> + * Returns 1 if the tag is present, 0 if it is not, negative error otherwise.
> + */
> +static int prog_has_dynload_tag(struct bpf_object *obj, struct bpf_program *prog)
> +{
> + int i, n;
> +
> + if (!obj->btf)
> + return 0;
> +
> + n = btf__type_cnt(obj->btf);
> + for (i = 1; i < n; i++) {
> + const struct btf_type *tag_t, *fn_t;
> +
> + tag_t = btf__type_by_id(obj->btf, i);
> + if (!btf_is_decl_tag(tag_t) || btf_decl_tag(tag_t)->component_idx != -1)
> + continue;
> + if (strcmp(btf__str_by_offset(obj->btf, tag_t->name_off),
> + DYNLOAD_DECL_TAG) != 0)
> + continue;
> +
> + /* a decl tag's type field points at the entity it decorates */
> + fn_t = btf__type_by_id(obj->btf, tag_t->type);
> + if (!btf_is_func(fn_t) || btf_func_linkage(fn_t) != BTF_FUNC_GLOBAL) {
> + pr_warn("prog '%s': " DYNLOAD_DECL_TAG
> + " decl tag must be applied to a global function\n",
> + prog->name);
> + return -EINVAL;
> + }
> +
> + if (strcmp(btf__str_by_offset(obj->btf, fn_t->name_off), prog->name) != 0)
> + continue; /* tag belongs to a different program */
> +
> + return 1;
> + }
> +
> + return 0;
> +}
> +
> static struct {
> enum bpf_prog_type prog_type;
> const char *ctx_name;
[ ... ]
> @@ -8467,6 +8516,22 @@ static int bpf_object_init_progs(struct bpf_object *obj, const struct bpf_object
> return err;
> }
> }
> +
> + if (prog_is_subprog(obj, prog))
> + continue;
> +
> + err = prog_has_dynload_tag(obj, prog);
> + if (err < 0)
> + return err;
> + if (err > 0) {
> + err = bpf_program__set_load_type(prog, BPF_PROG_LOAD_TYPE_DYNAMIC);
> + if (err) {
> + pr_warn("prog '%s': failed to apply " DYNLOAD_DECL_TAG
> + " decl tag: %s\n",
> + prog->name, errstr(err));
> + return err;
> + }
> + }
> }
>
> return 0;
Can the gen_loader incompatibility check reach __load_dynamic programs?
Looking at the control flow, bpf_object_init_progs() runs inside
bpf_object_open() before the object is returned to the caller, but
bpf_object__gen_loader() is a public API that can only be called after
open returns. This means obj->gen_loader is structurally guaranteed to
be NULL when prog_has_dynload_tag() routes through
bpf_program__set_load_type() -> bpf_program__set_dynamicload().
The guard in bpf_program__set_dynamicload() checks:
if (obj->gen_loader)
return libbpf_err(-ENOTSUP);
Both in-tree gen_loader users call bpf_object__open_file() first, then
bpf_object__gen_loader(), then bpf_object__load():
tools/bpf/bpftool/prog.c:
obj = bpf_object__open_file(file, &open_opts);
...
err = bpf_object__gen_loader(obj, &gen_opts);
...
err = bpf_object__load(obj);
tools/bpf/bpftool/gen.c:
err = bpf_object__gen_loader(obj, &gen_opts);
...
err = bpf_object__load(obj);
So running 'bpftool gen skeleton -L' on an object containing a
__load_dynamic program accepts the combination that the code declares
unsupported. The imperative bpf_program__set_dynamicload() call path
after open would correctly reject it, but the declarative path during
open cannot.
bpf_object__load_progs() then skips the program because load_type is
DYNAMIC, so bpf_gen__prog_load() is never emitted for it and
gen->nr_progs undercounts. The bpf_gen__finish() check only errors on
undercounts in the other direction, so the fd-writeback loop copies
prog_fd[i] into descriptor slot i for i < gen->nr_progs while bpftool
emits one struct bpf_prog_desc per program in object order. When the
dynamic program is not last, every following program's fd lands in the
wrong skel->progs.* slot.
Does this need an obj->gen_loader check in bpf_object_init_progs() after
prog_has_dynload_tag() returns 1, or should bpf_object__gen_loader() walk
the programs and error if it finds any with load_type == DYNAMIC?
> @@ -15248,10 +15313,9 @@ static int bpf_program__set_dynamicload(struct bpf_program *prog)
> * to gen_loader. For this reason, prevent calling
> * bpf_program__set_dynamicload when gen_loader was used to
> * generate a BPF object loader.
> - * A gen_loader implementation is being called for autoloaded
> - * programs and defines its own model for loading BPF programs.
> - * To pass a BPF program to gen_loader, set the program's load type
> - * to LD_AUTOLOAD.
> + * A gen_loader implementation is being called for programs with
> + * load type BPF_PROG_LOAD_TYPE_AUTO and defines its own model for
> + * loading BPF programs.
> */
> if (obj->gen_loader)
> return libbpf_err(-ENOTSUP);
---
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/33000627295
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 2/5] libbpf: BPF programs dynamic loading and attaching
2026-08-26 18:18 ` [PATCH bpf-next v2 2/5] libbpf: BPF programs dynamic loading and attaching Andrey Grodzovsky
2026-08-26 18:33 ` sashiko-bot
@ 2026-08-26 19:38 ` bot+bpf-ci
2026-09-11 23:44 ` Andrii Nakryiko
2 siblings, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-08-26 19:38 UTC (permalink / raw)
To: andrey.grodzovsky, bpf, andrii
Cc: martin.kelly, slava.imameev, linux-open-source, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 15443 bytes --]
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index 821d854d43a9f..c3fe1de432108 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
> @@ -725,6 +725,7 @@ struct bpf_object {
>
> bool has_subcalls;
> bool has_rodata;
> + bool has_dynload_progs;
>
> struct bpf_gen *gen_loader;
[ ... ]
> @@ -9076,15 +9078,22 @@ static void bpf_object_cleanup_btf(struct bpf_object *obj)
> obj->btf_modules_loaded = false;
> zfree(&obj->btf_modules);
>
> - /* clean up vmlinux BTF */
> - btf__free(obj->btf_vmlinux);
> - obj->btf_vmlinux = NULL;
> + /* The btf_vmlinux data is needed for dynamically loaded programs,
> + * so defer freeing it in that case to the end of the object lifetime.
> + */
> + if (!obj->has_dynload_progs) {
> + btf__free(obj->btf_vmlinux);
> + obj->btf_vmlinux = NULL;
> + }
This isn't a bug, but the BPF multi-line comment convention for
tools/lib/bpf/ requires the opening /* on a line of its own.
> @@ -9089,8 +9098,12 @@ static void bpf_object_post_load_cleanup(struct bpf_object *obj)
>
> static void bpf_object_post_load_cleanup(struct bpf_object *obj)
> {
> - /* clean up fd_array */
> - zfree(&obj->fd_array);
> + /* The fd array is needed for dynamically loaded programs,
> + * so defer freeing it in that case to the end of the object lifetime.
> + */
> + if (!obj->has_dynload_progs || !obj->fd_array_cnt)
> + zfree(&obj->fd_array);
>
> /* clean up BTF */
> bpf_object_cleanup_btf(obj);
Does this code keep the fd_array while closing the file descriptors it
contains?
The patch guards the zfree() to keep fd_array alive for dynamic programs,
but then immediately calls bpf_object_cleanup_btf(), which closes the
module BTF FDs:
bpf_object_cleanup_btf() {
for (i = 0; i < obj->btf_module_cnt; i++) {
close(obj->btf_modules[i].fd);
...
}
}
The only values stored in obj->fd_array are module BTF FDs from
obj->btf_modules[] (load_module_btfs() does 'obj->fd_array[obj->fd_array_cnt++]
= mod_btf->fd;'), so when bpf_program__load_dynamically() later runs
bpf_object_load_prog() with load_attr.fd_array = obj->fd_array, it hands
the verifier a table of closed FDs.
Best case the program load fails with -EBADF and dynamic loading simply
does not work for module kfuncs. Worse, if the FD number has been recycled
by any other open() in the process between object load and dynamic load,
the verifier resolves the kfunc against an unrelated file.
The patch guards btf_vmlinux in bpf_object_cleanup_btf() but leaves the
module-BTF close unconditional.
> @@ -9805,8 +9816,16 @@ bool bpf_program__autoload(const struct bpf_program *prog)
>
> int bpf_program__set_autoload(struct bpf_program *prog, bool autoload)
> {
> - return bpf_program__set_load_type(prog,
> - autoload ? BPF_PROG_LOAD_TYPE_AUTO : BPF_PROG_LOAD_TYPE_DISABLED);
> + enum bpf_prog_load_type type = prog->load_type;
> +
> + if (autoload)
> + type = BPF_PROG_LOAD_TYPE_AUTO;
> + else if (prog->load_type == BPF_PROG_LOAD_TYPE_AUTO)
> + type = BPF_PROG_LOAD_TYPE_DISABLED;
> + else
> + return 0; /* Otherwise, keep the current load type. */
> +
> + return bpf_program__set_load_type(prog, type);
> }
Does the early return skip the post-load check?
The 'else return 0;' arm bypasses the call to bpf_program__set_load_type(),
which is the only place that checks 'if (prog->obj->state >= OBJ_LOADED)
return libbpf_err(-EINVAL);'. After bpf_object__load(), calling
bpf_program__set_autoload(prog, false) on any program whose load type is
not AUTO (every skipped program and every dynamic program) now returns 0
instead of -EINVAL and leaves errno untouched.
Callers that use the return value to detect "too late to change autoload"
silently get a success where they used to get an error.
[ ... ]
> @@ -15220,12 +15239,73 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
> free(s);
> }
>
> +static int bpf_program__set_dynamicload(struct bpf_program *prog)
> +{
> + struct bpf_object *obj;
> + const char *attach_name;
> +
> + obj = prog->obj;
> + if (!obj)
> + return libbpf_err(-EINVAL);
> +
> + /* Dynamically-loaded programs are not supported for gen_loader.
> + * This is because bpf_object_load_prog is not called for
> + * dynamicload programs, so dynamicload programs are not visible
> + * to gen_loader. For this reason, prevent calling
> + * bpf_program__set_dynamicload when gen_loader was used to
> + * generate a BPF object loader.
> + * A gen_loader implementation is being called for autoloaded
> + * programs and defines its own model for loading BPF programs.
> + * To pass a BPF program to gen_loader, set the program's load type
> + * to LD_AUTOLOAD.
> + */
This isn't a bug, but does LD_AUTOLOAD exist? The only occurrence of
LD_AUTOLOAD in the tree is this comment. The enumerator the sentence
means is BPF_PROG_LOAD_TYPE_AUTO.
> + if (obj->gen_loader)
> + return libbpf_err(-ENOTSUP);
> +
> + if (prog_is_subprog(obj, prog))
> + return libbpf_err(-EINVAL);
> +
> + attach_name = strchr(prog->sec_name, '/');
> + if (!attach_name || strchr(attach_name, ':')) {
> + /* Only reject programs that require BTF-based attach target
> + * resolution (indicated by the SEC_ATTACH_BTF flag). Such
> + * programs need the section name parsed for the attach target
> + * function name (after '/') and optionally the module name
> + * (before ':') for libbpf_find_attach_btf_id.
> + *
> + * Programs like SEC("classifier"), SEC("socket"), etc. do
> + * not require BTF attach resolution and can safely use
> + * dynamic loading despite having no '/' in their section
> + * name. The BTF guard in libbpf_prepare_prog_load (checking
> + * SEC_ATTACH_BTF) is the authoritative check; this is an
> + * early-reject for programs that would fail there.
> + */
> + long flags = prog->sec_def ?
> + (long)prog->sec_def->cookie : SEC_ATTACH_BTF;
> + if ((flags & SEC_ATTACH_BTF) && !prog->attach_btf_id)
> + return libbpf_err(-EINVAL);
> + }
Does this reject section forms that libbpf supports?
1. The 'strchr(attach_name, ':')' check rejects the module-qualified
form SEC("fentry/<module>:<func>"). find_kernel_btf_id() explicitly
implements it:
find_kernel_btf_id() {
fn_name = strchr(attach_name, ':');
if (fn_name) {
mod_name = attach_name;
mod_len = fn_name - mod_name;
fn_name++;
}
}
and libbpf_prepare_prog_load() hands it 'strchr(prog->sec_name, '/') + 1'
verbatim, so SEC("fentry/<module>:<func>") resolves fine on the normal
load path (in-tree example: tools/testing/selftests/bpf/progs/
tracing_multi_attach_module.c). The comment even names the module part
as a supported input - "optionally the module name (before ':')" - and
then rejects exactly that.
2. The '!attach_name' check rejects SEC("fentry") with no declarative
target. libbpf_prepare_prog_load() supports this: its error message says
the target "is expected [to] be specified with bpf_program__set_attach_target()
at runtime before BPF object load step". Because the guard reads
prog->attach_btf_id, which set_attach_target() fills in, this makes the
two setters order-dependent: bpf_program__set_attach_target() then
bpf_program__set_load_type(DYNAMIC) succeeds, while the reverse order
fails with -EINVAL.
> +
> + obj->has_dynload_progs = true;
> + prog->load_type = BPF_PROG_LOAD_TYPE_DYNAMIC;
> + prog->autoattach = false;
> +
> + return 0;
> +}
Does this code forget to restore autoattach?
bpf_program__set_dynamicload() clears prog->autoattach as a side effect,
and nothing ever restores it. Because bpf_program__set_load_type() lets
a program move back out of BPF_PROG_LOAD_TYPE_DYNAMIC (its 'default:'
arm just assigns prog->load_type), this sequence leaves the program in
an inconsistent state:
bpf_program__set_load_type(prog, BPF_PROG_LOAD_TYPE_DYNAMIC);
/* autoattach = false */
bpf_program__set_load_type(prog, BPF_PROG_LOAD_TYPE_AUTO);
/* autoattach stays false */
The program is now autoloaded by bpf_object__load() but silently skipped
by the skeleton's attach step, and bpf_program__autoattach() reports
false even though the caller never touched it. The same happens through
bpf_program__set_autoload(prog, true) on a DYNAMIC program.
[ ... ]
> @@ -15233,3 +15313,49 @@ enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog)
> {
> return prog->load_type;
> }
> +
> +/*
> + * This function must be called after bpf_object__load_progs.
> + * Dynamically-loaded program data is initialized on object load.
> + * Post-load initialization is not supported.
> + */
> +int
> +bpf_program__load_dynamically(struct bpf_program *prog, int extra_log_level)
> +{
> + int err;
> + struct bpf_object *obj;
> +
> + obj = prog->obj;
> + if (!obj || obj->state < OBJ_LOADED)
> + return libbpf_err(-EINVAL);
> +
> + if (prog_is_subprog(obj, prog) || prog->load_type != BPF_PROG_LOAD_TYPE_DYNAMIC)
> + return libbpf_err(-EINVAL);
> +
> + prog->log_level |= extra_log_level;
> +
> + 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 dynamically load: %d\n", prog->name, err);
> + prog->log_level &= ~extra_log_level;
> + return err;
> + }
> +
> + prog->log_level &= ~extra_log_level;
> + return 0;
> +}
Does calling this twice leak the previous FD?
The function never checks whether prog already holds an FD, so calling
it twice without an intervening bpf_program__unload_dynamically() leaks
the previous prog FD and pins the previous kernel program instance for
the lifetime of the process.
bpf_object_load_prog() unconditionally overwrites the out parameter on
success:
*prog_fd = ret;
ret = 0;
goto out;
and the caller passes &prog->fd, so the old descriptor is simply dropped
with no close(). Because the leaked FD holds a reference on the old
bpf_prog, the kernel-side program (and its maps/BTF references) also stay
alive.
The commit message advertises this exact usage - "These programs can also
be reloaded and reattached multiple times" - so back-to-back loads are an
expected caller pattern, not an abuse.
The function should either reject a second load with -EBUSY when
prog->fd >= 0, or zclose(prog->fd) before loading.
---
Does the extra_log_level handling corrupt the program's log level?
The set-then-clear approach does not save and restore the original value.
With:
bpf_program__set_log_level(prog, 1);
bpf_program__load_dynamically(prog, 1);
the function does 'prog->log_level |= 1' (leaving it at 1), then
'prog->log_level &= ~1' (clearing it to 0). Any bit shared between the
caller's persistent log level and extra_log_level is lost.
bpf_program__log_level() now reports 0 instead of the value the caller
set, and any subsequent bpf_program__load_dynamically(prog, 0) loads
with no verifier log.
A correct save/restore needs the original value:
__u32 saved = prog->log_level;
prog->log_level |= extra_log_level;
...
prog->log_level = saved;
For comparison, bpf_object__load_progs() does 'prog->log_level |= log_level;'
with no restore at all, so the two paths also disagree on whether
extra_log_level is sticky.
---
A subsystem pattern flags this as potentially concerning: the error
return at the end of bpf_program__load_dynamically() is 'return err;'
instead of 'return libbpf_err(err);'. The two early -EINVAL paths in
the same function do use libbpf_err(), and the sibling function
bpf_program__unload_dynamically() wraps its returns, so the omission is
inconsistent within the patch itself.
bpf_object_load_prog() is an internal helper that returns kernel-style
negative error codes without touching errno. Concrete paths that return
an error with errno unset or stale:
- 'case BPF_PROG_TYPE_UNSPEC: ... return -EINVAL;'
- 'case BPF_PROG_TYPE_STRUCT_OPS: if (prog->attach_btf_id == 0) ...
return -EINVAL;'
- 'if (!insns || !insns_cnt) return -EINVAL;'
- 'err = prog->sec_def->prog_prepare_load_fn(...); if (err < 0) ...
return err;'
- 'ret = -ENOMEM; goto out;' on log-buffer realloc failure
On all of these, bpf_program__load_dynamically returns a negative value
while errno still holds whatever the caller last left there, so a caller
doing the documented 'if (bpf_program__load_dynamically(...)) perror(...)'
or reading errno gets a wrong or stale error.
Would it be safer to add the wrapper here to match the neighbouring
returns?
> diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
> index 700634664e326..54d7558612893 100644
> --- a/tools/lib/bpf/libbpf.h
> +++ b/tools/lib/bpf/libbpf.h
> @@ -2083,15 +2083,19 @@ LIBBPF_API int bpf_program__clone(struct bpf_program *prog, const struct bpf_pro
> *
> * - BPF_PROG_LOAD_TYPE_DISABLED: the program is not loaded.
> * - BPF_PROG_LOAD_TYPE_AUTO: the program is autoloaded when the bpf_object is loaded.
> + * - BPF_PROG_LOAD_TYPE_DYNAMIC: the program is loaded and attached dynamically.
> */
> enum bpf_prog_load_type {
> BPF_PROG_LOAD_TYPE_DISABLED = 0,
> BPF_PROG_LOAD_TYPE_AUTO,
> + BPF_PROG_LOAD_TYPE_DYNAMIC,
> };
>
> LIBBPF_API int bpf_program__set_load_type(struct bpf_program *prog,
> enum bpf_prog_load_type loadtype);
> LIBBPF_API enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog);
> +LIBBPF_API int bpf_program__load_dynamically(struct bpf_program *prog, int extra_log_level);
> +LIBBPF_API int bpf_program__unload_dynamically(struct bpf_program *prog);
[ ... ]
---
The v1 patch discussion with Andrii Nakryiko on lore (January-February
2025) raised design concerns that appear unaddressed. Three key points:
1. On Feb 5, 2025, Andrii stated: "I don't want to complicate bpf_object
internals with this third autoload state, as this creates tons of
non-obvious gotchas that have to constantly be checked whenever any new
feature is added to bpf_object (or even during refactorings)." He
proposed two alternatives: (1) splitting BPF programs into multiple
independent bpf_objects with shared maps via bpf_map__reuse_fd(), or (2)
implementing bpf_object__prepare() to handle relocations while the
application manages program loading. The discussion ended on Feb 25, 2025
without the author committing to either alternative approach.
Message-ID: CAEf4BzYWe0KCzA4-qwAGp5n_ydJ0_zyLSO=Crr_vewFHzZ0t6Q@mail.gmail.com
https://lore.kernel.org/bpf/CAEf4BzYWe0KCzA4-qwAGp5n_ydJ0_zyLSO=Crr_vewFHzZ0t6Q@mail.gmail.com/
2. On Feb 21, 2025, Andrii stated that bpf_object is intentionally
single-threaded and will not support parallel BPF program loading:
"bpf_object is intentionally single-threaded, so I don't think we'll be
supporting parallel BPF program loading in the paradigm of bpf_object...
I don't see bpf_object becoming multi-threaded. The dynamic program
loading/unloading/loading again is something that I can't yet justify,
tbh."
Message-ID: CAEf4BzZ=-r7AkSGhru_NMxPcXDLmkVpTiQrBxUvsgq-LE0Lk6w@mail.gmail.com
https://lore.kernel.org/bpf/CAEf4BzZ=-r7AkSGhru_NMxPcXDLmkVpTiQrBxUvsgq-LE0Lk6w@mail.gmail.com/
Has the patch design been revised to address these concerns, or does it
follow one of the suggested alternatives?
---
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/33000627295
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 0/5] libbpf: BPF program dynamic loading
2026-08-26 18:18 [PATCH bpf-next v2 0/5] libbpf: BPF program dynamic loading Andrey Grodzovsky
` (4 preceding siblings ...)
2026-08-26 18:18 ` [PATCH bpf-next v2 5/5] selftests/bpf: Cover BPF program dynamic loading Andrey Grodzovsky
@ 2026-09-11 23:43 ` Andrii Nakryiko
5 siblings, 0 replies; 20+ messages in thread
From: Andrii Nakryiko @ 2026-09-11 23:43 UTC (permalink / raw)
To: Andrey Grodzovsky, Mykyta Yatsenko
Cc: bpf, andrii, martin.kelly, slava.imameev, linux-open-source
On Wed, Aug 26, 2026 at 11:18 AM Andrey Grodzovsky
<andrey.grodzovsky@crowdstrike.com> wrote:
>
> This series was originally posted in January 2025 [1] as
> a two-patch RFC introducing a per-program tri-state load type
> (disabled/auto/dynamic) to let large BPF applications load a subset of
> their programs lazily, after the initial bpf_object load.
>
> We looked at the object-wide opt-in flag Andrii suggested as a
> lighter-weight alternative to a per-program enum ("all programs may load
> later, opt in via bpf_object_open_opts"). Problem was that it collapses two
> states that need to stay distinguishable at the level of a single program:[2]
>
> - a program that is not currently loaded but is *expected* to load
> later, gated on runtime configuration; and
> - a program that is *permanently* not going to load on this kernel
> (e.g. it targets a symbol or struct field that doesn't exist on the
> running kernel, or it lost out to an alternative implementation and
> will never be used on this object instance).
>
> A per-program enum keeps that distinction where it belongs: disabled
> stays fully excluded from relocation/map-creation with isolated failure
> handling, auto is unchanged, and the new dynamic state is the only one
> that opts a given program into later loading -- without implicitly
> promoting every other disabled program in the object along with it.
>
> This v2 keeps the same programs and tests we originally proposed, and
> addresses two additional requests from Andrii [3]:
>
> 1. bpf_program__set_autoload() is converted from bool to the new
> enum bpf_prog_load_type, so it remains source- and ABI-compatible
> with the ~200 existing bool callers (false->0, true->1 already match
> the enum's DISABLED/AUTO numbering), while becoming a thin forwarder
> to bpf_program__set_load_type(). It intentionally continues to reject
> BPF_PROG_LOAD_TYPE_DYNAMIC, to preserve its original on/off meaning --
> dynamic load is only reachable through set_load_type().
>
> 2. A declarative way to mark a program dynamic-load-eligible from its
> source, as an alternative to an imperative set_load_type() call, using
> a new BTF decl_tag.
>
> 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 dynamic loading helps in two ways:
>
> - it shortens the initial load, since only the programs actually
> needed for the running configuration are loaded and attached up
> front instead of the whole object; and
> - it lets a single failing program be unloaded and possibly
> reloaded (or a feature toggled at runtime) without tearing down
> and reloading the entire bpf_object.
>
> Both reduce the time window during which the tool is partially loaded or
> inactive -- for a security tool specifically, that is also the time
> window during which the system it protects is unprotected.
>
> We have been running this with our internal fork of libbpf in production
> for about a year. Depending on kernel and configuration support, around
> 250 of our programs are potentially loadable on any given system; with
> dynamic 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.
>
we should try to use this functionality in veristat, see if it can
more cleanly replace clone-based solution we have currently.
>
> [1] https://lore.kernel.org/bpf/20250122215206.59859-1-slava.imameev@crowdstrike.com/
> [2] https://lore.kernel.org/bpf/CAOu3gNjg5vQ=t0C5UmStNpK8zutFC7kcYTrKt9aiR8Gia+rdNw@mail.gmail.com/
> [3] https://lore.kernel.org/bpf/CAEf4BzZKy5Wv_TxtEqsbzW+v-tO28kgY=9RhXrnbPhOZcVbOSA@mail.gmail.com/
>
> Andrey Grodzovsky (2):
> libbpf: Convert bpf_program__set_autoload() to load-type enum
> libbpf: Support declarative dynamic load via BTF decl_tag
>
> Slava Imameev (3):
> libbpf: BPF program load type enum
> libbpf: BPF programs dynamic loading and attaching
> selftests/bpf: Cover BPF program dynamic loading
>
> tools/lib/bpf/bpf_helpers.h | 5 +
> tools/lib/bpf/libbpf.c | 273 ++++++++++++++++--
> tools/lib/bpf/libbpf.h | 32 +-
> tools/lib/bpf/libbpf.map | 4 +
> .../selftests/bpf/prog_tests/dynamicload.c | 222 ++++++++++++++
> .../selftests/bpf/prog_tests/load_type.c | 167 +++++++++++
> .../selftests/bpf/progs/test_dynamicload.c | 40 +++
> .../selftests/bpf/progs/test_load_type.c | 31 ++
> 8 files changed, 741 insertions(+), 33 deletions(-)
> create mode 100644 tools/testing/selftests/bpf/prog_tests/dynamicload.c
> create mode 100644 tools/testing/selftests/bpf/prog_tests/load_type.c
> create mode 100644 tools/testing/selftests/bpf/progs/test_dynamicload.c
> create mode 100644 tools/testing/selftests/bpf/progs/test_load_type.c
>
> --
> 2.34.1
>
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 1/5] libbpf: BPF program load type enum
2026-08-26 18:18 ` [PATCH bpf-next v2 1/5] libbpf: BPF program load type enum Andrey Grodzovsky
2026-08-26 19:25 ` bot+bpf-ci
@ 2026-09-11 23:43 ` Andrii Nakryiko
1 sibling, 0 replies; 20+ messages in thread
From: Andrii Nakryiko @ 2026-09-11 23:43 UTC (permalink / raw)
To: Andrey Grodzovsky
Cc: bpf, andrii, martin.kelly, slava.imameev, linux-open-source
On Wed, Aug 26, 2026 at 11:18 AM Andrey Grodzovsky
<andrey.grodzovsky@crowdstrike.com> wrote:
>
> From: Slava Imameev <slava.imameev@crowdstrike.com>
>
> Replacing the boolean field with an enum simplifies the addition
> of new load types. Currently, the bpf_program structure defines
> the autoload type using a boolean field. This field is now
> replaced with an enum, allowing new BPF program loading types
> to be introduced by extending the enum value range.
>
> Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
> Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
> ---
> tools/lib/bpf/libbpf.c | 50 +++++++++++++++++++++++++---------------
> tools/lib/bpf/libbpf.h | 15 ++++++++++++
> tools/lib/bpf/libbpf.map | 2 ++
> 3 files changed, 48 insertions(+), 19 deletions(-)
>
[...]
>
> bool bpf_program__autoattach(const struct bpf_program *prog)
> @@ -15111,7 +15109,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_type != BPF_PROG_LOAD_TYPE_AUTO || !prog->autoattach)
> continue;
>
> /* auto-attaching not supported for this program */
> @@ -15221,3 +15219,17 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
> free(s->progs);
> free(s);
> }
> +
> +int bpf_program__set_load_type(struct bpf_program *prog, enum bpf_prog_load_type type)
I'm a bit worried about confusion with "load type" being interpreted
as "program type at load". More precisely this would be "load
strategy", totally libbpf concept. A bit verbose, but this is also not
something used everywhere, so perhaps "set_load_strategy" is
acceptable. WDYT? Am I overthinking?
> +{
> + if (prog->obj->state >= OBJ_LOADED)
> + return libbpf_err(-EINVAL);
> +
> + prog->load_type = type;
> + return 0;
> +}
> +
> +enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog)
> +{
> + return prog->load_type;
> +}
> diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
> index b965ad571540..700634664e32 100644
> --- a/tools/lib/bpf/libbpf.h
> +++ b/tools/lib/bpf/libbpf.h
> @@ -2078,6 +2078,21 @@ 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 type:
> + *
> + * - BPF_PROG_LOAD_TYPE_DISABLED: the program is not loaded.
> + * - BPF_PROG_LOAD_TYPE_AUTO: the program is autoloaded when the bpf_object is loaded.
> + */
> +enum bpf_prog_load_type {
> + BPF_PROG_LOAD_TYPE_DISABLED = 0,
> + BPF_PROG_LOAD_TYPE_AUTO,
> +};
> +
> +LIBBPF_API int bpf_program__set_load_type(struct bpf_program *prog,
> + enum bpf_prog_load_type loadtype);
> +LIBBPF_API enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog);
> +
please add doc-comments for new APIs
> #ifdef __cplusplus
> } /* extern "C" */
> #endif
> diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
> index 08ab2ea881fb..4d63dac8c3b7 100644
> --- a/tools/lib/bpf/libbpf.map
> +++ b/tools/lib/bpf/libbpf.map
> @@ -462,4 +462,6 @@ LIBBPF_1.8.0 {
> bpf_program__clone;
> btf__find_by_name_kind_own;
> btf__new_empty_opts;
> + bpf_program__load_type;
> + bpf_program__set_load_type;
this list should be sorted
> } LIBBPF_1.7.0;
> --
> 2.34.1
>
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 2/5] libbpf: BPF programs dynamic loading and attaching
2026-08-26 18:18 ` [PATCH bpf-next v2 2/5] libbpf: BPF programs dynamic loading and attaching Andrey Grodzovsky
2026-08-26 18:33 ` sashiko-bot
2026-08-26 19:38 ` bot+bpf-ci
@ 2026-09-11 23:44 ` Andrii Nakryiko
2 siblings, 0 replies; 20+ messages in thread
From: Andrii Nakryiko @ 2026-09-11 23:44 UTC (permalink / raw)
To: Andrey Grodzovsky
Cc: bpf, andrii, martin.kelly, slava.imameev, linux-open-source
On Wed, Aug 26, 2026 at 11:18 AM Andrey Grodzovsky
<andrey.grodzovsky@crowdstrike.com> wrote:
>
> From: Slava Imameev <slava.imameev@crowdstrike.com>
>
> BPF programs designated as dynamically 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.
>
> Signed-off-by: Slava Imameev <slava.imameev@crowdstrike.com>
> Signed-off-by: Andrey Grodzovsky <andrey.grodzovsky@crowdstrike.com>
> ---
> tools/lib/bpf/libbpf.c | 144 ++++++++++++++++++++++++++++++++++++---
> tools/lib/bpf/libbpf.h | 4 ++
> tools/lib/bpf/libbpf.map | 2 +
> 3 files changed, 141 insertions(+), 9 deletions(-)
>
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index 821d854d43a9..c3fe1de43210 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
> @@ -725,6 +725,7 @@ struct bpf_object {
>
> bool has_subcalls;
> bool has_rodata;
> + bool has_dynload_progs;
>
> struct bpf_gen *gen_loader;
>
> @@ -8056,7 +8057,7 @@ static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog
> log_buf = prog->log_buf;
> log_buf_size = prog->log_size;
> own_log_buf = false;
> - } else if (obj->log_buf) {
> + } else if (obj->log_buf && prog->load_type != BPF_PROG_LOAD_TYPE_DYNAMIC) {
DYNAMIC as opposed to AUTO isn't all that clear a distinction, IMO. If
we go with "load strategy" for terminology, wouldn't this be AUTO vs
MANUAL?
> log_buf = obj->log_buf;
> log_buf_size = obj->log_size;
> own_log_buf = false;
> @@ -8403,6 +8404,7 @@ bpf_object__load_progs(struct bpf_object *obj, int log_level)
> pr_debug("prog '%s': skipped auto-loading\n", prog->name);
> continue;
> }
> +
> prog->log_level |= log_level;
>
> if (obj->gen_loader)
> @@ -9076,15 +9078,22 @@ static void bpf_object_cleanup_btf(struct bpf_object *obj)
> obj->btf_modules_loaded = false;
> zfree(&obj->btf_modules);
>
> - /* clean up vmlinux BTF */
> - btf__free(obj->btf_vmlinux);
> - obj->btf_vmlinux = NULL;
> + /* The btf_vmlinux data is needed for dynamically loaded programs,
nit: we've switched to more kernel-standard '/*' on separate line
style, please adjust comments in the next revision accordingly
> + * so defer freeing it in that case to the end of the object lifetime.
> + */
> + if (!obj->has_dynload_progs) {
> + btf__free(obj->btf_vmlinux);
> + obj->btf_vmlinux = NULL;
> + }
> }
>
> static void bpf_object_post_load_cleanup(struct bpf_object *obj)
> {
> - /* clean up fd_array */
> - zfree(&obj->fd_array);
> + /* The fd array is needed for dynamically loaded programs,
> + * so defer freeing it in that case to the end of the object lifetime.
> + */
> + if (!obj->has_dynload_progs || !obj->fd_array_cnt)
why the added fd_array_cnt condition?
> + zfree(&obj->fd_array);
>
> /* clean up BTF */
> bpf_object_cleanup_btf(obj);
> @@ -9678,6 +9687,8 @@ void bpf_object__close(struct bpf_object *obj)
> close(obj->jumptable_maps[i].fd);
> zfree(&obj->jumptable_maps);
>
> + zfree(&obj->fd_array);
> +
> free(obj);
> }
>
> @@ -9805,8 +9816,16 @@ bool bpf_program__autoload(const struct bpf_program *prog)
>
> int bpf_program__set_autoload(struct bpf_program *prog, bool autoload)
> {
> - return bpf_program__set_load_type(prog,
> - autoload ? BPF_PROG_LOAD_TYPE_AUTO : BPF_PROG_LOAD_TYPE_DISABLED);
> + enum bpf_prog_load_type type = prog->load_type;
> +
> + if (autoload)
> + type = BPF_PROG_LOAD_TYPE_AUTO;
> + else if (prog->load_type == BPF_PROG_LOAD_TYPE_AUTO)
> + type = BPF_PROG_LOAD_TYPE_DISABLED;
> + else
> + return 0; /* Otherwise, keep the current load type. */
why? should we unconditionally set DISABLED if autoload==false?
> +
> + return bpf_program__set_load_type(prog, type);
> }
>
> bool bpf_program__autoattach(const struct bpf_program *prog)
> @@ -15220,12 +15239,73 @@ void bpf_object__destroy_skeleton(struct bpf_object_skeleton *s)
> free(s);
> }
>
> +static int bpf_program__set_dynamicload(struct bpf_program *prog)
we use double underscore naming only for public APIs (apart from a
bunch of legacy functions)
but also why this separate function at all, just inline it in
set_load_type below
> +{
> + struct bpf_object *obj;
> + const char *attach_name;
> +
> + obj = prog->obj;
> + if (!obj)
> + return libbpf_err(-EINVAL);
do we ever check this anywhere?
> +
> + /* Dynamically-loaded programs are not supported for gen_loader.
> + * This is because bpf_object_load_prog is not called for
> + * dynamicload programs, so dynamicload programs are not visible
> + * to gen_loader. For this reason, prevent calling
> + * bpf_program__set_dynamicload when gen_loader was used to
> + * generate a BPF object loader.
> + * A gen_loader implementation is being called for autoloaded
> + * programs and defines its own model for loading BPF programs.
> + * To pass a BPF program to gen_loader, set the program's load type
> + * to LD_AUTOLOAD.
> + */
> + if (obj->gen_loader)
> + return libbpf_err(-ENOTSUP);
> +
ENOTSUP is kernel-internal, use EOPNOTSUPP
> + if (prog_is_subprog(obj, prog))
> + return libbpf_err(-EINVAL);
> +
> + attach_name = strchr(prog->sec_name, '/');
> + if (!attach_name || strchr(attach_name, ':')) {
> + /* Only reject programs that require BTF-based attach target
> + * resolution (indicated by the SEC_ATTACH_BTF flag). Such
> + * programs need the section name parsed for the attach target
> + * function name (after '/') and optionally the module name
> + * (before ':') for libbpf_find_attach_btf_id.
hm... this looks ugly... why BTF-base dattach targets can't be set to
dynamic/manual load?
> + *
> + * Programs like SEC("classifier"), SEC("socket"), etc. do
> + * not require BTF attach resolution and can safely use
> + * dynamic loading despite having no '/' in their section
> + * name. The BTF guard in libbpf_prepare_prog_load (checking
> + * SEC_ATTACH_BTF) is the authoritative check; this is an
> + * early-reject for programs that would fail there.
> + */
> + long flags = prog->sec_def ?
> + (long)prog->sec_def->cookie : SEC_ATTACH_BTF;
> + if ((flags & SEC_ATTACH_BTF) && !prog->attach_btf_id)
> + return libbpf_err(-EINVAL);
> + }
> +
> + obj->has_dynload_progs = true;
if someone sets load type to DISABLED from what used to be DYNAMIC
(e.g., because kernel is not supported), we don't recalculate
has_dynload_progs. I think we should determine has_dynload_progs at
the beginning of preparation step (and disable changing that), instead
of trying to keep track of it like you do here
pw-bot: cr
> + prog->load_type = BPF_PROG_LOAD_TYPE_DYNAMIC;
> + prog->autoattach = false;
> +
> + return 0;
> +}
> +
> int bpf_program__set_load_type(struct bpf_program *prog, enum bpf_prog_load_type type)
> {
> if (prog->obj->state >= OBJ_LOADED)
I think this should be OBJ_PREPARED, after bpf_object__prepare() is
called, it's too late to change this load type
> return libbpf_err(-EINVAL);
>
> - prog->load_type = type;
> + switch (type) {
> + case BPF_PROG_LOAD_TYPE_DYNAMIC:
> + return bpf_program__set_dynamicload(prog);
> + default:
let's have explicit case XXX listed, with default returning -EINVAL
> + prog->load_type = type;
> + break;
> + }
> +
> return 0;
> }
>
> @@ -15233,3 +15313,49 @@ enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog)
> {
> return prog->load_type;
> }
> +
> +/*
> + * This function must be called after bpf_object__load_progs.
any problems with it being called after bpf_object__prepare()? what's missing?
> + * Dynamically-loaded program data is initialized on object load.
> + * Post-load initialization is not supported.
> + */
> +int
> +bpf_program__load_dynamically(struct bpf_program *prog, int extra_log_level)
just call it bpf_program__load()
> +{
> + int err;
> + struct bpf_object *obj;
> +
> + obj = prog->obj;
> + if (!obj || obj->state < OBJ_LOADED)
prog->obj should always be set, stop checking for it
> + return libbpf_err(-EINVAL);
> +
> + if (prog_is_subprog(obj, prog) || prog->load_type != BPF_PROG_LOAD_TYPE_DYNAMIC)
we should also check that program is not already loaded (probably
enough to check prog->fd, double check)
> + return libbpf_err(-EINVAL);
> +
> + prog->log_level |= extra_log_level;
hm... why this extra_log_level? Why can't user just set
bpf_program__set_log_level() before attempting to bpf_program__load()
> +
> + 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 dynamically load: %d\n", prog->name, err);
> + prog->log_level &= ~extra_log_level;
> + return err;
> + }
> +
> + prog->log_level &= ~extra_log_level;
> + return 0;
> +}
> +
> +int bpf_program__unload_dynamically(struct bpf_program *prog)
> +{
> + int err;
> +
> + if (!prog || prog->load_type != BPF_PROG_LOAD_TYPE_DYNAMIC)
> + return libbpf_err(-EINVAL);
we don't expect NULL, unless it's explicitly specified as a
possibility. And you didn't check prog for NULL in
bpf_program__load_dynamically(). let's be consistent (and not check
for NULL)
> +
> + /* Close the file descriptor but retain the program's data to
> + * support reloading the program if it is required again.
> + */
> + err = zclose(prog->fd);
> + return err ? libbpf_err(-errno) : 0;
> +}
> diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
> index 700634664e32..54d755861289 100644
> --- a/tools/lib/bpf/libbpf.h
> +++ b/tools/lib/bpf/libbpf.h
> @@ -2083,15 +2083,19 @@ LIBBPF_API int bpf_program__clone(struct bpf_program *prog, const struct bpf_pro
> *
> * - BPF_PROG_LOAD_TYPE_DISABLED: the program is not loaded.
> * - BPF_PROG_LOAD_TYPE_AUTO: the program is autoloaded when the bpf_object is loaded.
> + * - BPF_PROG_LOAD_TYPE_DYNAMIC: the program is loaded and attached dynamically.
> */
> enum bpf_prog_load_type {
> BPF_PROG_LOAD_TYPE_DISABLED = 0,
> BPF_PROG_LOAD_TYPE_AUTO,
> + BPF_PROG_LOAD_TYPE_DYNAMIC,
> };
>
> LIBBPF_API int bpf_program__set_load_type(struct bpf_program *prog,
> enum bpf_prog_load_type loadtype);
> LIBBPF_API enum bpf_prog_load_type bpf_program__load_type(const struct bpf_program *prog);
> +LIBBPF_API int bpf_program__load_dynamically(struct bpf_program *prog, int extra_log_level);
> +LIBBPF_API int bpf_program__unload_dynamically(struct bpf_program *prog);
>
> #ifdef __cplusplus
> } /* extern "C" */
> diff --git a/tools/lib/bpf/libbpf.map b/tools/lib/bpf/libbpf.map
> index 4d63dac8c3b7..a0389c58efe9 100644
> --- a/tools/lib/bpf/libbpf.map
> +++ b/tools/lib/bpf/libbpf.map
> @@ -464,4 +464,6 @@ LIBBPF_1.8.0 {
> btf__new_empty_opts;
> bpf_program__load_type;
> bpf_program__set_load_type;
> + bpf_program__load_dynamically;
> + bpf_program__unload_dynamically;
> } LIBBPF_1.7.0;
> --
> 2.34.1
>
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 3/5] libbpf: Convert bpf_program__set_autoload() to load-type enum
2026-08-26 18:18 ` [PATCH bpf-next v2 3/5] libbpf: Convert bpf_program__set_autoload() to load-type enum Andrey Grodzovsky
2026-08-26 18:34 ` sashiko-bot
2026-08-26 19:25 ` bot+bpf-ci
@ 2026-09-11 23:44 ` Andrii Nakryiko
2 siblings, 0 replies; 20+ messages in thread
From: Andrii Nakryiko @ 2026-09-11 23:44 UTC (permalink / raw)
To: Andrey Grodzovsky
Cc: bpf, andrii, martin.kelly, slava.imameev, linux-open-source
On Wed, Aug 26, 2026 at 11:18 AM Andrey Grodzovsky
<andrey.grodzovsky@crowdstrike.com> wrote:
>
> Convert bpf_program__set_autoload() from bool to enum bpf_prog_load_type,
> to unify the API with the new tri-state load type. Reject
> BPF_PROG_LOAD_TYPE_DYNAMIC to preserve the original autoload on/off
> meaning; only BPF_PROG_LOAD_TYPE_DYNAMIC is dynamic-load-only. Also
> restore autoattach when leaving BPF_PROG_LOAD_TYPE_DYNAMIC via
> set_load_type(), fixing a bug where a program left dynamic load and
> became autoload-eligible but never re-attached.
>
> 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 | 29 ++++++++++++++++++-----------
> tools/lib/bpf/libbpf.h | 39 +++++++++++++++++++++++++--------------
> tools/lib/bpf/libbpf.map | 6 +++---
> 3 files changed, 46 insertions(+), 28 deletions(-)
>
if we go with bpf_program__set_load_type() (or whatever the name), I
don't think we need to change the existing API, let's just leave it
be.
[...]
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH bpf-next v2 5/5] selftests/bpf: Cover BPF program dynamic loading
2026-08-26 18:18 ` [PATCH bpf-next v2 5/5] selftests/bpf: Cover BPF program dynamic loading Andrey Grodzovsky
2026-08-26 18:33 ` sashiko-bot
2026-08-26 19:25 ` bot+bpf-ci
@ 2026-09-11 23:44 ` Andrii Nakryiko
2 siblings, 0 replies; 20+ messages in thread
From: Andrii Nakryiko @ 2026-09-11 23:44 UTC (permalink / raw)
To: Andrey Grodzovsky
Cc: bpf, andrii, martin.kelly, slava.imameev, linux-open-source
On Wed, Aug 26, 2026 at 11:18 AM Andrey Grodzovsky
<andrey.grodzovsky@crowdstrike.com> wrote:
>
> From: Slava Imameev <slava.imameev@crowdstrike.com>
>
> Add load_type and dynamicload tests: the per-program load type enum
> (disabled/auto/dynamic) and its transitions, set_autoload() bool/enum
> compatibility and autoattach restore on exit from dynamic load, the
> full dynamic load/attach/reattach/unload lifecycle, and the
> loadtype:dynamic BTF decl_tag as a declarative alternative to an
> imperative set_load_type() call.
>
> 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/dynamicload.c | 222 ++++++++++++++++++
> .../selftests/bpf/prog_tests/load_type.c | 167 +++++++++++++
> .../selftests/bpf/progs/test_dynamicload.c | 40 ++++
> .../selftests/bpf/progs/test_load_type.c | 31 +++
> 4 files changed, 460 insertions(+)
> create mode 100644 tools/testing/selftests/bpf/prog_tests/dynamicload.c
> create mode 100644 tools/testing/selftests/bpf/prog_tests/load_type.c
> create mode 100644 tools/testing/selftests/bpf/progs/test_dynamicload.c
> create mode 100644 tools/testing/selftests/bpf/progs/test_load_type.c
>
I could barely find that __load_dynamic tag, seems a bit subtle. Maybe
we should just go with SEC() prefix like with SEC("?..."). E.g.,
SEC("!...") to mark manual loading. Seems to kind of fit, exclamation
mark calls for attention, no? WDYT?
> diff --git a/tools/testing/selftests/bpf/prog_tests/dynamicload.c b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
> new file mode 100644
> index 000000000000..799c43034ff9
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/dynamicload.c
> @@ -0,0 +1,222 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +#include <test_progs.h>
> +#include <time.h>
> +#include "test_dynamicload.skel.h"
> +
> +/* prog4 is tagged __load_dynamic in the source instead of being set
> + * imperatively; verify that an explicit bpf_program__set_load_type() call
> + * before load overrides the tag's declarative default.
> + */
[...]
^ permalink raw reply [flat|nested] 20+ messages in thread
end of thread, other threads:[~2026-09-11 23:44 UTC | newest]
Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-26 18:18 [PATCH bpf-next v2 0/5] libbpf: BPF program dynamic loading Andrey Grodzovsky
2026-08-26 18:18 ` [PATCH bpf-next v2 1/5] libbpf: BPF program load type enum Andrey Grodzovsky
2026-08-26 19:25 ` bot+bpf-ci
2026-09-11 23:43 ` Andrii Nakryiko
2026-08-26 18:18 ` [PATCH bpf-next v2 2/5] libbpf: BPF programs dynamic loading and attaching Andrey Grodzovsky
2026-08-26 18:33 ` sashiko-bot
2026-08-26 19:38 ` bot+bpf-ci
2026-09-11 23:44 ` Andrii Nakryiko
2026-08-26 18:18 ` [PATCH bpf-next v2 3/5] libbpf: Convert bpf_program__set_autoload() to load-type enum Andrey Grodzovsky
2026-08-26 18:34 ` sashiko-bot
2026-08-26 19:25 ` bot+bpf-ci
2026-09-11 23:44 ` Andrii Nakryiko
2026-08-26 18:18 ` [PATCH bpf-next v2 4/5] libbpf: Support declarative dynamic load via BTF decl_tag Andrey Grodzovsky
2026-08-26 18:33 ` sashiko-bot
2026-08-26 19:25 ` bot+bpf-ci
2026-08-26 18:18 ` [PATCH bpf-next v2 5/5] selftests/bpf: Cover BPF program dynamic loading Andrey Grodzovsky
2026-08-26 18:33 ` sashiko-bot
2026-08-26 19:25 ` bot+bpf-ci
2026-09-11 23:44 ` Andrii Nakryiko
2026-09-11 23:43 ` [PATCH bpf-next v2 0/5] libbpf: " Andrii Nakryiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox